diff --git a/.gitignore b/.gitignore index 48a9ee7..07e3800 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,10 @@ out/ ## VSCode ############################## .vscode/ + +############################## +## Misc +############################## +.settings/* +.project +.classpath diff --git a/google_checks.xml b/google_checks.xml new file mode 100644 index 0000000..2434e8d --- /dev/null +++ b/google_checks.xml @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index b0f6764..f9a8b88 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,13 @@ org.openjfx javafx-controls ${javafx.version} + win + + + org.openjfx + javafx-graphics + ${javafx.version} + win @@ -38,7 +45,30 @@ spring-security-crypto 7.0.2 - + + + tools.jackson.core + jackson-databind + 3.1.0 + compile + + + org.springframework + spring-core + 6.1.10 + + + org.slf4j + slf4j-simple + 2.0.9 + + + com.h2database + h2 + 2.2.224 + runtime + + @@ -86,6 +116,41 @@ maven-javadoc-plugin 3.12.0 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + package + + shade + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + META-INF/NOTICE + META-INF/LICENSE + META-INF.versions.9.module-info + + + + + + edu.group5.app.App + + + + + + diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index 7fd22a6..d657fb8 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -1,10 +1,100 @@ package edu.group5.app; +import edu.group5.app.control.MainController; +import edu.group5.app.control.wrapper.DbWrapper; +import edu.group5.app.control.wrapper.OrgApiWrapper; +import edu.group5.app.model.donation.Donation; +import edu.group5.app.model.donation.DonationRepository; +import edu.group5.app.model.donation.DonationService; +import edu.group5.app.model.organization.OrganizationRepository; +import edu.group5.app.model.organization.OrganizationService; +import edu.group5.app.model.user.User; +import edu.group5.app.model.user.UserRepository; +import edu.group5.app.model.user.UserService; +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.stage.Stage; + +import java.util.List; + +import java.util.logging.Logger; + /** - * Hello world! + * Main entry point for the Help-Me-Help charity donation application. + * Handles database connection, data loading, and application setup. */ -public class App { - public static void main(String[] args) { - System.out.println("Hello World!"); +public class App extends Application { + DbWrapper dbWrapper; + UserRepository userRepository; + DonationRepository donationRepository; + private Logger logger; + private MainController controller; + private Scene scene; + + @Override + public void init() { + this.logger = Logger.getLogger(App.class.getName()); + this.logger.info("Application starting"); + + this.dbWrapper = new DbWrapper(false); + OrgApiWrapper orgApiWrapper = new OrgApiWrapper("https://app.innsamlingskontrollen.no/api/public/v1/all"); + + while (!dbWrapper.connect()) { + this.logger.warning("Failed to connect to database"); + } + + // Load data from database + List userData = dbWrapper.importUsers(); + List donationData = dbWrapper.fetchAllDonations(); + dbWrapper.disconnect(); + + // Load organizations from API + Object[] organizationData = new Object[0]; + try { + if (orgApiWrapper.importData()) { + organizationData = orgApiWrapper.getData(); + } + } catch (InterruptedException e) { + System.err.println("Failed to load organization data: " + e.getMessage()); } + + // Create repositories with fetched data + this.userRepository = new UserRepository(userData); + this.donationRepository = new DonationRepository(donationData); + OrganizationRepository organizationRepository = new OrganizationRepository(organizationData); + + // Create services (backend wiring) + UserService userService = new UserService(this.userRepository); + DonationService donationService = new DonationService(this.donationRepository, organizationRepository); + OrganizationService organizationService = new OrganizationService(organizationRepository); + + this.controller = new MainController(userService, donationService, organizationService); + + this.scene = controller.getMainView().getScene(); + } + + @Override + public void start(Stage stage) { + this.controller.showLoginPage(); + + stage.getIcons().add(new Image(getClass().getResource("/header/images/hmh-logo.png").toExternalForm())); + stage.setTitle("Help-Me-Help"); + stage.setScene(this.scene); + stage.show(); + } + + @Override + public void stop() throws Exception { + super.stop(); + this.logger.info("Application stopping"); + this.dbWrapper.connect(); + this.dbWrapper.exportUsers(this.userRepository.export()); + this.dbWrapper.exportDonations(this.donationRepository.export()); + this.dbWrapper.disconnect(); + } + + public static void main(String[] args) { + launch(args); + } } diff --git a/src/main/java/edu/group5/app/control/BrowseCardController.java b/src/main/java/edu/group5/app/control/BrowseCardController.java new file mode 100644 index 0000000..9f86271 --- /dev/null +++ b/src/main/java/edu/group5/app/control/BrowseCardController.java @@ -0,0 +1,16 @@ +package edu.group5.app.control; + +import edu.group5.app.model.organization.Organization; + +public class BrowseCardController { + private final MainController controller; + + public BrowseCardController(MainController mainController) { + this.controller = mainController; + } + + public void handleCardClick(Organization organization) { + controller.setCurrentOrganization(organization); + controller.showOrganizationPage(); + } +} diff --git a/src/main/java/edu/group5/app/control/BrowsePageController.java b/src/main/java/edu/group5/app/control/BrowsePageController.java new file mode 100644 index 0000000..0922e68 --- /dev/null +++ b/src/main/java/edu/group5/app/control/BrowsePageController.java @@ -0,0 +1,9 @@ +package edu.group5.app.control; + +public class BrowsePageController { + private final MainController controller; + + public BrowsePageController(MainController mainController) { + this.controller = mainController; + } +} diff --git a/src/main/java/edu/group5/app/control/HeaderController.java b/src/main/java/edu/group5/app/control/HeaderController.java new file mode 100644 index 0000000..0a1e424 --- /dev/null +++ b/src/main/java/edu/group5/app/control/HeaderController.java @@ -0,0 +1,28 @@ +package edu.group5.app.control; + +public class HeaderController { + private final MainController controller; + + public HeaderController(MainController controller) { + this.controller = controller; + } + + public void handleHomeBtn() { + System.out.println("Home button pressed"); + controller.showHomePage(); + } + + public void handleCausesBtn() { + System.out.println("Causes button pressed"); + controller.showBrowsePage(); + } + + public void handleAboutBtn() { + System.out.println("About button pressed"); + } + + public void handleProfileBtn() { + System.out.println("profileSection"); + controller.showUserPage(); + } +} diff --git a/src/main/java/edu/group5/app/control/HomePageController.java b/src/main/java/edu/group5/app/control/HomePageController.java new file mode 100644 index 0000000..10d3fbf --- /dev/null +++ b/src/main/java/edu/group5/app/control/HomePageController.java @@ -0,0 +1,19 @@ +package edu.group5.app.control; + +public class HomePageController { + private final MainController controller; + + public HomePageController(MainController controller) { + this.controller = controller; + } + + public void handleDonateToACauseBtn() { + System.out.println("Donate to a cause button pressed"); + controller.showBrowsePage(); + } + + public void handleAboutUsBtn() { + System.out.println("About us button pressed"); + controller.showAboutUsPage(); + } +} diff --git a/src/main/java/edu/group5/app/control/LoginPageController.java b/src/main/java/edu/group5/app/control/LoginPageController.java new file mode 100644 index 0000000..b28f191 --- /dev/null +++ b/src/main/java/edu/group5/app/control/LoginPageController.java @@ -0,0 +1,44 @@ +package edu.group5.app.control; + +import edu.group5.app.model.user.User; +import edu.group5.app.model.user.UserService; +import edu.group5.app.view.loginpage.LoginPageView; + +public class LoginPageController { + private final MainController controller; + private final UserService userService; + private LoginPageView view; + + public LoginPageController(MainController controller, UserService userService) { + this.controller = controller; + this.userService = userService; + } + + public void setView(LoginPageView view) { + this.view = view; + } + + public void handleLoginBtn() { + String email = view.getEmail(); + char[] passwordChars = view.getPassword(); + + if (email == null || email.trim().isEmpty() || passwordChars == null || passwordChars.length == 0) { + view.showError("Email and password are required"); + return; + } + + User user = userService.login(email, passwordChars); + + if (user != null) { + controller.setCurrentUser(user); + controller.showHomePage(); + } else { + view.showError("Invalid email or password"); + } + } + + public void handleRegisterBtn() { + System.out.println("Sign in button pressed"); + controller.showSignInPage(); + } +} diff --git a/src/main/java/edu/group5/app/control/MainController.java b/src/main/java/edu/group5/app/control/MainController.java new file mode 100644 index 0000000..2b75487 --- /dev/null +++ b/src/main/java/edu/group5/app/control/MainController.java @@ -0,0 +1,121 @@ +package edu.group5.app.control; + +import edu.group5.app.control.donationpage.DonationPageController; +import edu.group5.app.model.donation.DonationService; +import edu.group5.app.model.organization.Organization; +import edu.group5.app.model.organization.OrganizationService; +import edu.group5.app.model.user.User; +import edu.group5.app.model.user.UserService; +import edu.group5.app.view.MainView; +import edu.group5.app.view.donationpage.DonationPageView; + +import java.math.BigDecimal; + +public class MainController { + private final MainView view; + private final HeaderController headerController; + private final HomePageController homePageController; + private final BrowsePageController browsePageController; + private final BrowseCardController browseCardController; + private final OrganizationPageController organizationPageController; + private final DonationPageController donationPageController; + private final UserService userService; + private final DonationService donationService; + private final OrganizationService organizationService; + private User currentUser; + private Organization currentOrganization; + private BigDecimal currentDonationAmount; + + public MainController(UserService userService, DonationService donationService, + OrganizationService organizationService) { + this.userService = userService; + this.donationService = donationService; + this.organizationService = organizationService; + + this.view = new MainView(this, userService); + this.headerController = new HeaderController(this); + this.homePageController = new HomePageController(this); + this.browsePageController = new BrowsePageController(this); + this.browseCardController = new BrowseCardController(this); + this.organizationPageController = new OrganizationPageController(this); + this.donationPageController = new DonationPageController(this); + } + + public UserService getUserService() { + return userService; + } + + public DonationService getDonationService() { + return donationService; + } + + public OrganizationService getOrganizationService() { + return organizationService; + } + + public void setCurrentUser(User user) { + this.currentUser = user; + } + + public User getCurrentUser() { + return this.currentUser; + } + + public void setCurrentOrganization(Organization organization) { + this.currentOrganization = organization; + } + + public Organization getCurrentOrganization() { + return this.currentOrganization; + } + + public void setCurrentDonationAmount(BigDecimal amount) { + this.currentDonationAmount = amount; + } + + public BigDecimal getCurrentDonationAmount() { + return this.currentDonationAmount; + } + + public void logout() { + currentUser = null; + currentOrganization = null; + currentDonationAmount = null; + showLoginPage(); + } + + public MainView getMainView() { + return view; + } + + public void showHomePage() { + view.showHomePage(homePageController, headerController); + } + + public void showLoginPage() { + view.showLoginPage(); + } + public void showSignInPage() { + view.showSignInPage(); + } + public void showPaymentCompletePage() { + view.showPaymentCompletePage(); + } + public void showBrowsePage() { + view.showBrowsePage(browsePageController, browseCardController, headerController); + } + + public void showOrganizationPage() { + view.showOrganizationPage(organizationPageController, headerController); + } + + public void showDonationPage() { + view.showDonationPage(); + } + + public void showAboutUsPage() {} + + public void showUserPage() { + view.showUserPage(); + } +} diff --git a/src/main/java/edu/group5/app/control/OrganizationPageController.java b/src/main/java/edu/group5/app/control/OrganizationPageController.java new file mode 100644 index 0000000..dd8f1bf --- /dev/null +++ b/src/main/java/edu/group5/app/control/OrganizationPageController.java @@ -0,0 +1,13 @@ +package edu.group5.app.control; + +public class OrganizationPageController { + private final MainController controller; + + public OrganizationPageController(MainController controller) { + this.controller = controller; + } + + public void handleDonateClick() { + controller.showDonationPage(); + } +} diff --git a/src/main/java/edu/group5/app/control/SignInPageController.java b/src/main/java/edu/group5/app/control/SignInPageController.java new file mode 100644 index 0000000..2d8d874 --- /dev/null +++ b/src/main/java/edu/group5/app/control/SignInPageController.java @@ -0,0 +1,58 @@ +package edu.group5.app.control; +import edu.group5.app.model.user.UserService; +import edu.group5.app.view.loginpage.SignInPageView; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import edu.group5.app.model.user.User; + +public class SignInPageController { + private final MainController controller; + private final UserService userService; + private SignInPageView view; + + public SignInPageController(MainController controller, UserService userService) { + this.controller = controller; + this.userService = userService; + } + + public void setView(SignInPageView view) { + this.view = view; + } + + public void handleSignInBtn() { + String firstName = view.getFirstName(); + String lastName = view.getLastName(); + String email = view.getEmail(); + char[] passwordChars = view.getPassword(); + + if (firstName == null || firstName.trim().isEmpty() || + lastName == null || lastName.trim().isEmpty() || + email == null || email.trim().isEmpty() || + passwordChars == null || passwordChars.length == 0) { + view.showError("All fields are required"); + return; + } + + String password = new String(passwordChars); + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + String hashedPassword = encoder.encode(password); + + boolean success = userService.registerUser( + "Customer", firstName, lastName, email, hashedPassword); + + if (success) { + User user = userService.getUserByEmail(email); + + controller.setCurrentUser(user); + controller.showHomePage(); + } else { + view.showError("Registration failed. Email may already be in use."); + } + } + + public void handleLoginBtn() { + System.out.println("Back to login button pressed"); + controller.showLoginPage(); + } +} diff --git a/src/main/java/edu/group5/app/control/Wrapper.java b/src/main/java/edu/group5/app/control/Wrapper.java deleted file mode 100644 index 9655c26..0000000 --- a/src/main/java/edu/group5/app/control/Wrapper.java +++ /dev/null @@ -1,5 +0,0 @@ -package edu.group5.app.control; - -public class Wrapper { - -} diff --git a/src/main/java/edu/group5/app/control/donationpage/DonationPageController.java b/src/main/java/edu/group5/app/control/donationpage/DonationPageController.java new file mode 100644 index 0000000..5fed3e2 --- /dev/null +++ b/src/main/java/edu/group5/app/control/donationpage/DonationPageController.java @@ -0,0 +1,60 @@ +package edu.group5.app.control.donationpage; + +import edu.group5.app.control.MainController; +import edu.group5.app.model.organization.Organization; +import edu.group5.app.model.user.Customer; +import edu.group5.app.model.user.User; + +import java.math.BigDecimal; + +public class DonationPageController { + private final MainController controller; + + public DonationPageController(MainController controller) { + this.controller = controller; + } + public void handleDonationBtn() { + // Get session data from MainController + User currentUser = controller.getCurrentUser(); + Organization currentOrg = controller.getCurrentOrganization(); + BigDecimal amount = controller.getCurrentDonationAmount(); + + if (currentUser == null) { + System.err.println("Error: No user logged in"); + return; + } + if (!(currentUser instanceof Customer customer)) { + System.err.println("Error: Only customers can donate"); + return; + } + if (currentOrg == null) { + System.err.println("Error: No organization selected"); + return; + } + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + System.err.println("Error: Invalid donation amount"); + return; + } + + // Create donation via service + boolean success = controller.getDonationService().donate( + customer, + currentOrg.orgNumber(), + amount, + "Online" + ); + + if (success) { + System.out.println("Donation created: " + amount + " kr to " + currentOrg.name()); + } else { + System.err.println("Failed to create donation"); + } + + // Clear donation session state + controller.setCurrentDonationAmount(null); + + // Navigate to payment complete + controller.showPaymentCompletePage(); + } + +} diff --git a/src/main/java/edu/group5/app/control/donationpage/PaymentCompleteController.java b/src/main/java/edu/group5/app/control/donationpage/PaymentCompleteController.java new file mode 100644 index 0000000..d1bc3a5 --- /dev/null +++ b/src/main/java/edu/group5/app/control/donationpage/PaymentCompleteController.java @@ -0,0 +1,15 @@ +package edu.group5.app.control.donationpage; + +import edu.group5.app.control.MainController; + +public class PaymentCompleteController { + private final MainController controller; + + public PaymentCompleteController(MainController controller) { + this.controller = controller; + } + public void handleHomeBtn() { + System.out.println("Home button pressed"); + controller.showHomePage(); + } +} diff --git a/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java b/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java new file mode 100644 index 0000000..7e3adc0 --- /dev/null +++ b/src/main/java/edu/group5/app/control/wrapper/DbWrapper.java @@ -0,0 +1,238 @@ +package edu.group5.app.control.wrapper; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class DbWrapper { + protected Connection connection; + private static final String CONNECTION_TYPE = "jdbc:h2:"; + private static final String DB_SCRIPT = "INIT=RUNSCRIPT FROM 'classpath:"; + private String connectionString; + private List users; + private List donations; + private Logger logger = Logger.getLogger(DbWrapper.class.getName()); + + public DbWrapper(boolean test) { + if (test) { + this.connectionString = CONNECTION_TYPE + "mem:test;" + DB_SCRIPT + "test_init.sql'"; + } else { + this.connectionString = CONNECTION_TYPE + "file:./help-me-help;" + DB_SCRIPT + "init.sql'"; + } + this.logger.info("connectionString constructed"); + } + + public boolean connect() { + try { + this.connection = DriverManager.getConnection(this.connectionString); + if (this.connection.isValid(0)) { + this.logger.info("Database connected"); + return true; + } else { + this.logger.warning("Failed to connect to database"); + return false; + } + } catch (SQLException e) { + this.logger.log(Level.SEVERE, "Failed to connect to database due to exception", e); + return false; + } + } + + public boolean disconnect() { + try{ this.connection.close(); } catch (Exception e) {}; + try { + return this.connection.isClosed(); + } catch (Exception e) { + this.logger.log(Level.WARNING, "Failed to check if connection is closed due to exception", e); + return false; + } + } + + private void close(ResultSet results, PreparedStatement ps) { + try { results.close(); } catch (Exception e) {} + try { ps.close(); } catch (Exception e) {} + this.logger.info("results and ps closed"); + } + + public List importUsers() { + PreparedStatement ps = null; + ResultSet results = null; + try{ + ps = this.connection.prepareStatement("SELECT * FROM users"); + results = ps.executeQuery(); + List data = new ArrayList(); + while (results.next()) { + data.add( + new Object[] { + results.getInt("user_id"), + results.getString("role"), + results.getString("first_name"), + results.getString("last_name"), + results.getString("email"), + results.getString("password_hash") + }); + } + this.users = data; + this.logger.info("Users imported"); + } catch (SQLException e) { + this.logger.log(Level.SEVERE, "Unexpected SQL exception", e); + } finally { + this.close(results, ps); + } + return this.users; + } + + public int exportUsers(List data) { + this.importUsers(); + + if (data == null) { + throw new IllegalArgumentException("data can't be null"); + } + if (data.isEmpty()) { + return 0; + } + if (data.get(0).length != 6) { + throw new IllegalArgumentException("data's arrays must have a length of 6"); + } + if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { + throw new IllegalArgumentException("One or more rows in data contains null values"); + } + if (this.users.size() > 0) { + if ((int) data.getLast()[0] <= (int) this.users.getLast()[0]) { + throw new IllegalArgumentException("data can't contain existing rows"); + } + } + Set ids = new HashSet<>(); + if (data.stream().anyMatch(i -> !ids.add(i[0]))) { + throw new IllegalArgumentException("data can't contain duplicate rows"); + } + + + PreparedStatement ps = null; + int rowsAffected = 0; + try { + ps = this.connection.prepareStatement( + "INSERT INTO users (user_id, role, first_name, last_name, email, password_hash) VALUES (?, ?, ?, ?, ?, ?)"); + for (Object[] row : data) { + try { + ps.setInt(1, (int) row[0]); + for (int i = 1; i < row.length; i++) { + ps.setString(i + 1, (String) row[i]); + } + } catch (Exception e) { + throw new IllegalArgumentException("One or more rows in data contains a wrong datatype"); + } + rowsAffected += ps.executeUpdate(); + } + this.logger.info("Users exported"); + } catch (SQLException e) { + this.logger.log(Level.SEVERE, "Unexpected SQL exception", e); + } finally { + this.close(null, ps); + } + return rowsAffected; + } + + public List fetchAllDonations() { + return this.importDonations(0, true); + } + + public List importDonations(int user_id) { + return this.importDonations(user_id, false); + } + + private List importDonations(int user_id, boolean all) { + PreparedStatement ps = null; + ResultSet results = null; + try{ + if (all) { + ps = this.connection.prepareStatement("SELECT * FROM donations"); + } else { + ps = this.connection.prepareStatement("SELECT * FROM donations WHERE user_id = ?"); + ps.setInt(1, user_id); + } + results = ps.executeQuery(); + List data = new ArrayList(); + while (results.next()) { + data.add( + new Object[] { + results.getInt(1), + results.getInt(2), + results.getInt(3), + results.getBigDecimal(4), + results.getTimestamp(5), + results.getString(6) + }); + } + this.donations = data; + this.logger.info("Donations imported"); + } catch (SQLException e) { + this.logger.log(Level.SEVERE, "Unexpected SQL exception", e); + } finally { + this.close(results, ps); + } + return this.donations; + } + + public int exportDonations(List data) { + this.fetchAllDonations(); + + if (data == null) { + throw new IllegalArgumentException("data can't be null"); + } + if (data.isEmpty()) { + return 0; + } + if (data.get(0).length != 6) { + throw new IllegalArgumentException("data's arrays must have a length of 6"); + } + if (data.stream().anyMatch(i -> Arrays.asList(i).contains(null))) { + throw new IllegalArgumentException("One or more rows in data contains null values"); + } + if (this.donations.size() > 0 && (int) data.getLast()[0] <= (int) this.donations.getLast()[0]) { + throw new IllegalArgumentException("data can't contain existing rows"); + } + Set ids = new HashSet<>(); + if (data.stream().anyMatch(i -> !ids.add(i[0]))) { + throw new IllegalArgumentException("data can't contain duplicate rows"); + } + + PreparedStatement ps = null; + int rowsAffected = 0; + try { + ps = this.connection.prepareStatement( + "INSERT INTO donations (donation_id, user_id, organization_id, amount, dating, payment_method) VALUES (?, (SELECT user_id FROM users WHERE user_id = ?), ?, ?, ?, ?)"); + for (Object[] row : data) { + try { + for (int i = 0; i < 3; i++) { + ps.setInt(i + 1, (int) row[i]); + } + ps.setBigDecimal(4, (BigDecimal) row[3]); + ps.setTimestamp(5, (Timestamp) row[4]); + ps.setString(6, (String) row[5]); + } catch (Exception e) { + throw new IllegalArgumentException("One or more rows in data contains a wrong datatype"); + } + rowsAffected += ps.executeUpdate(); + } + this.logger.info("Donations exported"); + } catch (SQLException e) { + this.logger.log(Level.SEVERE, "Unexpected SQL exception", e); + } finally { + this.close(null, ps); + } + return rowsAffected; + } + +} diff --git a/src/main/java/edu/group5/app/control/wrapper/OrgApiWrapper.java b/src/main/java/edu/group5/app/control/wrapper/OrgApiWrapper.java new file mode 100644 index 0000000..39ac283 --- /dev/null +++ b/src/main/java/edu/group5/app/control/wrapper/OrgApiWrapper.java @@ -0,0 +1,80 @@ +package edu.group5.app.control.wrapper; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import tools.jackson.core.exc.StreamReadException; +import tools.jackson.databind.ObjectMapper; + +/** + * A Class for Wrapping an API. + */ +public class OrgApiWrapper extends Wrapper { + private Object[] data; + private HttpClient client; + private HttpRequest request; + + /** + * The constructor, which takes a url String and constructs a URI and + * HttpRequest object from it. + * If the url is invalid, it will throw a fitting exception. + * + * @param urlString A string of the URL that's being connected to. + */ + public OrgApiWrapper(String urlString) { + if (urlString == null) { + throw new IllegalArgumentException("url can't be null"); + } else if (urlString.isBlank()) { + throw new IllegalArgumentException("url can't be blank"); + } + try { + URI uri = URI.create(urlString); + this.client = HttpClient.newHttpClient(); + this.request = HttpRequest.newBuilder() + .uri(uri) + .GET() + .build(); + } catch (IllegalArgumentException IAe) { + throw new IllegalArgumentException("url has to be valid"); + } + + } + + /** + * A method for importing data from the wrapped API. + * + * @return Returns a boolean, which indicates if the import was successful. Will + * be False if, for + * example, there is no internet connection. + * + * @throws InterruptedException This exception is thrown whenever the program is + * interrupted, like + * by ctrl + c. + */ + @Override + public boolean importData() throws InterruptedException { + try { + HttpResponse response = this.client.send( + this.request, HttpResponse.BodyHandlers.ofString()); + this.data = new ObjectMapper().readValue(response.body(), Object[].class); + return true; + } catch (IOException IOe) { + return false; + } catch (StreamReadException e) { + throw new StreamReadException("The URL leads to a website that can't be parsed"); + } + } + + /** + * A method for accessing the imported data. + * + * @return Returns an array with HashMaps, which is how data is structured in + * the API. + */ + @Override + public Object[] getData() { + return this.data; + } +} diff --git a/src/main/java/edu/group5/app/control/wrapper/Wrapper.java b/src/main/java/edu/group5/app/control/wrapper/Wrapper.java new file mode 100644 index 0000000..992b7a9 --- /dev/null +++ b/src/main/java/edu/group5/app/control/wrapper/Wrapper.java @@ -0,0 +1,31 @@ +package edu.group5.app.control.wrapper; + +/** + * An abstract class for all Wrappers of datasets. + */ +abstract class Wrapper { + + protected Wrapper() { + } + + /** + * An abstract method for importing data from the dataset that child methods + * wrap. + * + * @return Returns a boolean, which indicates if the import was successful. Will + * be False if, for + * example, there is no internet connection. + * + * @throws InterruptedException This exception is thrown whenever the program is + * interrupted, like + * by ctrl + c. + */ + public abstract boolean importData() throws InterruptedException; + + /** + * An abstract method to access the imported data. + * + * @return Returns a fitting parsed Object directly from the dataset. + */ + public abstract Object getData(); +} diff --git a/src/main/java/edu/group5/app/model/DBRepository.java b/src/main/java/edu/group5/app/model/DBRepository.java new file mode 100644 index 0000000..3cd1fa9 --- /dev/null +++ b/src/main/java/edu/group5/app/model/DBRepository.java @@ -0,0 +1,43 @@ +package edu.group5.app.model; + +import java.util.HashMap; +import java.util.Map; +import java.util.List; + +/** + * Abstract base class for repositories that store their data + * in a database-related structure. + * + *

+ * Extends {@link Repository} and specifies that the content + * is stored as a {@link Map}. + *

+ */ +public abstract class DBRepository extends Repository { + protected final Map contentLock; + + /** + * Constructs a DBRepository with the given content. + * + * @param content the HashMap used to store repository entities + */ + protected DBRepository(Map content) { + super(content); + this.contentLock = new HashMap<>(); + } + + protected abstract void updateContentLock(); + + public abstract boolean addContent(V value); + + /** + * Exports the repository content as a list of Object arrays, where each array + * represents a row of data. + * This method is intended for converting the repository content into a format + * suitable for database storage or export. + * + * @return a List of Object arrays representing the repository content for + * database export + */ + public abstract List export(); +} diff --git a/src/main/java/edu/group5/app/model/Repository.java b/src/main/java/edu/group5/app/model/Repository.java new file mode 100644 index 0000000..032f307 --- /dev/null +++ b/src/main/java/edu/group5/app/model/Repository.java @@ -0,0 +1,26 @@ +package edu.group5.app.model; + +import java.util.Map; + +/** + * Represents a repository. + */ +public abstract class Repository { + protected final Map content; + /** + * Constructs a new Repository with the specified content. + * + * @param content the underlying data structure used to store entities + */ + protected Repository(Map content) { + this.content = content; + } + + /** + * Gets the content of the repo + * @return content of the repo + */ + public Map getContent() { + return content; + } +} diff --git a/src/main/java/edu/group5/app/model/donation/Donation.java b/src/main/java/edu/group5/app/model/donation/Donation.java index a127439..92adfbe 100644 --- a/src/main/java/edu/group5/app/model/donation/Donation.java +++ b/src/main/java/edu/group5/app/model/donation/Donation.java @@ -1,5 +1,53 @@ package edu.group5.app.model.donation; -public class Donation { - +import java.math.BigDecimal; +import java.sql.Timestamp; + +/** + * Represents a verified donation made by a user to an organization. + * @param donationId - the unique ID of this donation + * @param userId - the ID of the user making the donation + * @param organizationId - the ID of the organization receiving the donation + * @param amount - the donation amount + * @param date - the timestamp when the donation was made + * @param paymentMethod - the payment method used + */ +public record Donation( + int donationId, + int userId, + int organizationId, + BigDecimal amount, + Timestamp date, + String paymentMethod) { + + /** + * Constructor with throws. + * + * @param donationId - throws if donationID is negative or 0. + * @param userId - throws if userID is negative or 0. + * @param organizationId - throws if organizationID is negative or 0. + * @param amount - throws if amount is negative or null. + * @param date - throws if date is null. + * @param paymentMethod - throws if payment is null or blank. + */ + public Donation { + if (donationId <= 0) { + throw new IllegalArgumentException("Donation ID must be positive"); + } + if (userId <= 0) { + throw new IllegalArgumentException("User ID must be positive"); + } + if (organizationId <= 0) { + throw new IllegalArgumentException("Organization ID must be positive"); + } + if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Amount must be positive and not null"); + } + if (date == null) { + throw new IllegalArgumentException("Date must not be null"); + } + if (paymentMethod == null || paymentMethod.isBlank()) { + throw new IllegalArgumentException("Payment method must not be empty"); + } + } } diff --git a/src/main/java/edu/group5/app/model/donation/DonationRepository.java b/src/main/java/edu/group5/app/model/donation/DonationRepository.java new file mode 100644 index 0000000..a55ea74 --- /dev/null +++ b/src/main/java/edu/group5/app/model/donation/DonationRepository.java @@ -0,0 +1,215 @@ +package edu.group5.app.model.donation; + +import edu.group5.app.model.DBRepository; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.List; +import java.math.BigDecimal; +import java.sql.Timestamp; + +/** + * Repository class for Donation. + * + *

+ * Extends {@link DBRepository} and manages Donation entities. + *

+ */ +public class DonationRepository extends DBRepository { + private final HashMap content; + + /** + * Constructs DonationRepository from a list of Object[] rows from the database. + * + * @param rows List of Object[] representing donations from the DB. + * Each row must have 6 elements: + * [donationId, userId, organizationId, amount, date, paymentMethod] + * @throws IllegalArgumentException if the input list is null or any row is + * invalid + */ + public DonationRepository(List rows) { + super(new HashMap<>()); + if (rows == null) { + throw new IllegalArgumentException("The list of rows cannot be null"); + } + this.content = new HashMap<>(); + for (Object[] row : rows) { + if (row == null || row.length != 6) { + throw new IllegalArgumentException("Each row must contain exactly 6 elements"); + } + int donationId = (int) row[0]; + int customerId = (int) row[1]; + int organizationId = (int) row[2]; + BigDecimal amount = (BigDecimal) row[3]; + Timestamp date = (Timestamp) row[4]; + String paymentMethod = (String) row[5]; + + Donation donation = new Donation(donationId, customerId, organizationId, amount, date, paymentMethod); + this.content.put(donationId, donation); + } + this.updateContentLock(); + } + + @Override + protected void updateContentLock() { + synchronized (contentLock) { + this.contentLock.clear(); + this.contentLock.putAll(this.content); + } + } + + @Override + public List export() { + Map output = new HashMap<>(this.content); + for (int i : super.contentLock.keySet()) { + output.remove(i); + } + this.updateContentLock(); + return output.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(entry -> { + Donation donation = entry.getValue(); + return new Object[] { + donation.donationId(), donation.userId(), + donation.organizationId(), donation.amount(), + donation.date(), donation.paymentMethod() }; + }) + .toList(); + } + + /** + * Retrieves a donation by its ID. + * + * @param donationId the ID of the donation to retrieve + * @return the Donation object with the specified ID, or null if not found + * @throws IllegalArgumentException if the donationId is not positive + */ + public Donation getDonationById(int donationId) { + if (donationId <= 0) { + throw new IllegalArgumentException("Donation ID must be positive"); + } + return content.get(donationId); + } + + /** + * Generates the next donation ID based on the current maximum ID in the + * repository. + * + * @return the next donation ID to be used for a new donation + */ + public int getNextDonationId() { + return content.keySet().stream().max(Integer::compareTo).orElse(0) + 1; + } /* TODO change this when data database is introduced */ + + public Map getAllDonations() { + return new HashMap<>(content); + } + + /** + * Adds a new donation to the repository + *

+ * The donation is stored using its {@code donationId} as the key. + * If a donation with the same ID already exists, the donation + * will not be added. + *

+ * + * @param donation the donation to add + * @return {@code true} if the donation was successfully added, and + * {@code false} if a donation with the same ID already exists + */ + @Override + public boolean addContent(Donation donation) { + if (donation == null) { + throw new IllegalArgumentException("Donation cannot be null"); + } + if (content.containsKey(donation.donationId())) { + return false; + } + this.content.put(donation.donationId(), donation); + return true; + } + + /** + * Returns all donations sorted by date (ascending). + * + *

+ * The returned map preserves the sorted order. + *

+ * + * @return a new {@link HashMap} containing the donations sorted by date + */ + public HashMap sortByDate() { + return content.entrySet().stream() + .sorted(Map.Entry.comparingByValue( + Comparator.comparing(Donation::date))) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new)); + } + + /** + * Returns all donations sorted by amount (ascending). + * + *

+ * The returned map preserves the sorted order from lowest to highest amount. + *

+ * + * @return a new {@link HashMap} containing the donations sorted by amount. + */ + public HashMap sortByAmount() { + return content.entrySet().stream() + .sorted(Map.Entry.comparingByValue( + Comparator.comparing(Donation::amount))) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new)); + } + + /** + * Returns all donations associated with a specific organization. + * + * @param orgNumber the organization ID to filter by + * @return a map containing all donations that belong to the given organization + * @throws IllegalArgumentException if the orgNumber is not positive + */ + public HashMap filterByOrganization(int orgNumber) { + if (orgNumber <= 0) { + throw new IllegalArgumentException("Organization number must be positive"); + } + return content.entrySet() + .stream() + .filter(entry -> entry.getValue().organizationId() == orgNumber) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new)); + } + + /** + * Returns all donations made by a specific user. + * + * @param userId the user ID to filter by + * @return a map containing all donations that belong to the given user + * @throws IllegalArgumentException if the userId is not positive + */ + public HashMap filterByUser(int userId) { + if (userId <= 0) { + throw new IllegalArgumentException("User ID must be positive"); + } + return content.entrySet().stream() + .filter(entry -> entry.getValue().userId() == userId) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new)); + } +} diff --git a/src/main/java/edu/group5/app/model/donation/DonationService.java b/src/main/java/edu/group5/app/model/donation/DonationService.java new file mode 100644 index 0000000..690a632 --- /dev/null +++ b/src/main/java/edu/group5/app/model/donation/DonationService.java @@ -0,0 +1,81 @@ +package edu.group5.app.model.donation; +import java.time.Instant; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import edu.group5.app.model.organization.Organization; +import edu.group5.app.model.organization.OrganizationRepository; +import edu.group5.app.model.user.Customer; + +/** + * DonationService class provides functionality for handling donations in the system. + * It interacts with the DonationRepository to manage donation records + * and the OrganizationRepository to validate organization information. + * The donate method allows a customer to make a donation to a specified organization, + * ensuring that the customer, organization, and donation amount are valid before processing the donation. + */ +public class DonationService { + + private final DonationRepository donationRepository; + private final OrganizationRepository organizationRepository; + + /** + * Constructor for DonationService. Initializes the service with the required repositories. + * @param donationRepository the repository for managing donation records + * @param organizationRepository the repository for managing organization information + * @throws IllegalArgumentException if either repository is null + */ + public DonationService(DonationRepository donationRepository, + OrganizationRepository organizationRepository) { + if (donationRepository == null) { + throw new IllegalArgumentException("DonationRepository cannot be null"); + } + if (organizationRepository == null) { + throw new IllegalArgumentException("OrganizationRepository cannot be null"); + } + this.donationRepository = donationRepository; + this.organizationRepository = organizationRepository; + } + + /** + * Getter for the DonationRepository used by this service. + * This method allows access to the donation repository for managing donation records and retrieving donation information. + * @return the DonationRepository instance used by this service + */ + public DonationRepository getDonationRepository() { + return this.donationRepository; + } + + /** + * Getter for the OrganizationRepository used by this service. + * This method allows access to the organization repository for validating organization information when processing donations. + * @return the OrganizationRepository instance used by this service + */ + public OrganizationRepository getOrganizationRepository() { + return this.organizationRepository; + } + + /** + * Processes a donation from a customer to a specified organization with a given amount. + * Validates the customer, organization number, and donation amount before creating a donation record. + * @param customer the customer making the donation + * @param orgNumber the organization number to which the donation is made + * @param amount the amount of the donation + * @return true if the donation is successfully processed, false otherwise + */ + public boolean donate(Customer customer, int orgNumber, BigDecimal amount, String paymentMethod) { + if (customer == null || amount == null + || amount.compareTo(BigDecimal.ZERO) <= 0 || paymentMethod.isBlank()) { + return false; + } + Organization org = organizationRepository.findByOrgNumber(orgNumber); + if (org == null) { + return false; + } + Donation donation = + new Donation(donationRepository.getNextDonationId(), + customer.getUserId(), org.orgNumber(), amount, Timestamp.from(Instant.now()), paymentMethod); + this.donationRepository.addContent(donation); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/edu/group5/app/model/organization/Organization.java b/src/main/java/edu/group5/app/model/organization/Organization.java index 825945f..42844e1 100644 --- a/src/main/java/edu/group5/app/model/organization/Organization.java +++ b/src/main/java/edu/group5/app/model/organization/Organization.java @@ -1,5 +1,61 @@ package edu.group5.app.model.organization; -public class Organization { - +import java.util.Objects; + +/** + * Represents an organization. + * + *

+ * An organization is identified by an organization number, a name, + * trust status, website Url, pre-approval status, and a textual description. + * + *

+ * Instances are validated on creation: + *

    + *
  • orgNumber must be non-negative
  • + *
  • name and websiteUrl must not be null or blank
  • + *
  • description must not be null
  • + *
+ */ +public record Organization( + int orgNumber, + String name, + boolean trusted, + String websiteUrl, + boolean isPreApproved, + String description) { + /** + * Creates a new organization. + * + * @param orgNumber the organization number; must be non-negative + * @param name the organization name; must not be null or blank + * @param trusted whether the organization is trusted + * @param websiteUrl the organization's website Url; must not be null or + * blank + * @param isPreApproved whether the organization is pre-approved + * @param description a textual description of the organization; must not be + * null + * @throws NullPointerException if name, websiteUrl or description is null + * @throws IllegalArgumentException if orgNumber is negative, or if name or + * websiteUrl is blank + */ + public Organization(int orgNumber, String name, boolean trusted, String websiteUrl, boolean isPreApproved, + String description) { + if (orgNumber < 0) { + throw new IllegalArgumentException("orgNumber cannot be negative"); + } + this.orgNumber = orgNumber; + this.name = Objects.requireNonNull(name, "name cannot be null"); + this.trusted = trusted; + this.websiteUrl = Objects.requireNonNull(websiteUrl, "websiteUrl cannot be null"); + this.isPreApproved = isPreApproved; + this.description = Objects.requireNonNull(description, "description cannot be null"); + + if (name.isBlank()) { + throw new IllegalArgumentException("name cannot be blank"); + } + if (websiteUrl.isBlank()) { + throw new IllegalArgumentException("websiteUrl cannot be blank"); + } + } } diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java new file mode 100644 index 0000000..a47b3d5 --- /dev/null +++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java @@ -0,0 +1,116 @@ +package edu.group5.app.model.organization; + +import edu.group5.app.model.Repository; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; + +import java.util.HashMap; +import java.util.Map; + +/** + * Repository class for managing Organization entities. It provides methods to retrieve trusted organizations, + * find organizations by their organization number or name, and initializes the repository with input data. + * The repository uses a HashMap to store Organization objects for efficient retrieval based on their organization number. + * Handles the business logic associated with organizations + */ +public class OrganizationRepository extends Repository { + private final HashMap grandMap; + + /** + * Initializes the repository with the given input data, c + * onverting it into Organization objects and storing them in a map for efficient retrieval. + * The input is expected to be an array of objects, where each object contains + * the necessary information to create an Organization. + * @param input the input data used to populate the repository, must not be null + * @throws IllegalArgumentException if the input is null + */ + public OrganizationRepository(Object[] input) { + super(new HashMap<>()); + grandMap = new HashMap<>(); + if (input == null) { + throw new IllegalArgumentException("The input cannot be null"); + } + ObjectMapper mapper = new ObjectMapper(); + + for (Object obj : input) { + HashMap contentMap = + mapper.convertValue(obj, new TypeReference>() {}); + + String orgNumberStr = ((String) contentMap.get("org_number")).replaceAll("\\s", ""); + int orgNumber = Integer.parseInt(orgNumberStr); + String name = (String) contentMap.get("name"); + boolean trusted = "approved".equalsIgnoreCase((String) contentMap.get("status")); + String websiteURL = (String) contentMap.get("url"); + boolean isPreApproved = Boolean.TRUE.equals(contentMap.get("is_pre_approved")); + String description = "Information about " + name; + Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description); + + grandMap.put(org.orgNumber(), org); + } + } + + /** + * Exports the organization data in a format suitable for output, such as JSON. Each organization is represented as a map containing its attributes. + * @return an array of maps, where each map represents an organization with its attributes (org_number, name, status, url, is_pre_approved) + * @throws IllegalStateException if the repository is empty + */ + public Object[] export() { + if (grandMap.isEmpty()) { + throw new IllegalStateException("The repository is empty"); + } + return grandMap.values().stream() + .map(org -> { Map orgMap = new HashMap<>(); + orgMap.put("org_number", org.orgNumber()); + orgMap.put("name", org.name()); + orgMap.put("status", org.trusted() ? "approved" : "unapproved"); + orgMap.put("url", org.websiteUrl()); + orgMap.put("is_pre_approved", org.isPreApproved()); + return orgMap; + }) + .toArray(); + } + +/** + * Gets all trusted organizations in the repository + * @return all organizations with trusted = true + */ + public Map getTrustedOrganizations() { + Map trustedOrganizations = new HashMap<>(); + + grandMap.forEach((orgNr, org) -> { + if (org.trusted()) { + trustedOrganizations.put(orgNr, org); + } + }); + return trustedOrganizations; + } + + /** + * Finds an organization by its organization number + * @param orgNumber the organization number of the Organization + * @return the Organization with the given organization number, or null if not found + * @throws IllegalArgumentException if the organization number is not a positive integer + */ + public Organization findByOrgNumber(int orgNumber) { + if (orgNumber <= 0) { + throw new IllegalArgumentException("The Organization number must be a positive integer"); + } + return grandMap.get(orgNumber); + } + + /** + * Finds an organization by its name, ignoring case + * @param name the name of the Organization + * @return the Organization with the given name, or null if not found + * @throws IllegalArgumentException if the name is null or empty + */ + public Organization findByOrgName(String name) { + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("The name cannot be null"); + } + return grandMap.values().stream() + .filter(org -> org.name().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationService.java b/src/main/java/edu/group5/app/model/organization/OrganizationService.java new file mode 100644 index 0000000..c5979f5 --- /dev/null +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -0,0 +1,58 @@ +package edu.group5.app.model.organization; + +import java.util.Map; + +/** + * Service class for managing organization-related operations. + * It interacts with the OrganizationRepository to retrieve organization information + * and contains business logic associated with organization management. + */ +public class OrganizationService { + private OrganizationRepository organizationRepository; + + /** + * Constructs an OrganizationService with the given OrganizationRepository. + * @param organizationRepository the OrganizationRepository to use for managing organization data; must not be null + * @throws IllegalArgumentException if organizationRepository is null + */ + public OrganizationService(OrganizationRepository organizationRepository) { + if (organizationRepository == null) { + throw new IllegalArgumentException("OrganizationRepository cannot be null"); + } + this.organizationRepository = organizationRepository; + } + + /** + * Getter for the OrganizationRepository used by this service. + * @return the OrganizationRepository instance used by this service + */ + public OrganizationRepository getOrganizationRepository() { + return this.organizationRepository; + } + + /** + * Retrieves all trusted organizations. + * @return a map of trusted organizations by organization number + */ + public Map getTrustedOrganizations() { + return organizationRepository.getTrustedOrganizations(); + } + + /** + * Finds an organization by its organization number. + * @param orgNumber the organization number to find + * @return the Organization if found, null otherwise + */ + public Organization findByOrgNumber(int orgNumber) { + return organizationRepository.findByOrgNumber(orgNumber); + } + + /** + * Finds an organization by its name. + * @param name the name of the organization + * @return the Organization if found, null otherwise + */ + public Organization findByOrgName(String name) { + return organizationRepository.findByOrgName(name); + } +} diff --git a/src/main/java/edu/group5/app/model/user/Customer.java b/src/main/java/edu/group5/app/model/user/Customer.java new file mode 100644 index 0000000..93f03ac --- /dev/null +++ b/src/main/java/edu/group5/app/model/user/Customer.java @@ -0,0 +1,59 @@ +package edu.group5.app.model.user; + +import java.util.List; +import java.util.ArrayList; + +/** + * Customer class represents a customer in the system. + * It extends the User class and includes additional functionality specific to customers, + * such as managing their preferences for organizations. + * Each customer has a map of preferences that associates organization numbers with the corresponding Organization objects. + */ +public class Customer extends User { + private List preferences; +/** + * Constructs a new Customer object for new registrations. + * The role is automatically set to "Customer" and the preferences list is initialized empty. + * + * @param userId the unique identifier for the user, must be positive + * @param firstName the first name of the customer + * @param lastName the last name of the customer + * @param email the email address of the customer + * @param passwordHash the hashed password of the customer + * @throws IllegalArgumentException if any parameter is invalid (null, empty, or userId ≤ 0) + */ +public Customer(int userId, String firstName, String lastName, + String email, String passwordHash) { + super(userId, firstName, lastName, email, passwordHash); + this.preferences = new ArrayList<>(); +} + + public List getPreferences() { + return preferences; + } + + @Override + public String getRole() { + return UserRepository.ROLE_CUSTOMER; + } + + public void addPreference(int orgNumber) { + if (orgNumber <= 0) { + throw new IllegalArgumentException("Organization number must be a positive integer"); + } + if (preferences.contains(orgNumber)) { + throw new IllegalArgumentException("Organization number already in preferences"); + } + preferences.add(orgNumber); + } + + public void removePreference(int orgNumber) { + if (orgNumber <= 0) { + throw new IllegalArgumentException("Organization number must be a positive integer"); + } + if (!preferences.contains(orgNumber)) { + throw new IllegalArgumentException("Organization number not found in preferences"); + } + preferences.remove(Integer.valueOf(orgNumber)); + } +} \ No newline at end of file diff --git a/src/main/java/edu/group5/app/model/user/User.java b/src/main/java/edu/group5/app/model/user/User.java index e163785..411538d 100644 --- a/src/main/java/edu/group5/app/model/user/User.java +++ b/src/main/java/edu/group5/app/model/user/User.java @@ -1,5 +1,117 @@ package edu.group5.app.model.user; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +/** + * User class represents a user in the system. It is an abstract class that will be extended by specific user types such as Donor, Recipient, and Admin. + * Each user has a unique userId, a role that defines their permissions in the system, and personal information such as first name, last name, email, and password hash. + * The constructor validates that all required fields are provided and throws an IllegalArgumentException if any of the fields are null or empty. + * This ensures that the User objects are always in a valid state when created. + * The class also includes a method to verify the user's password + * by comparing the provided plaintext password with the stored hashed password using BCrypt. + * + */ +public abstract class User { + private int userId; + private String firstName; + private String lastName; + private String email; + private String passwordHash; + + /** + * Constructor for User class. Validates that all required fields + * are provided and throws an IllegalArgumentException if any of the fields are null or empty. + * @param userId the unique identifier for the user, must be a positive integer + * @param firstName the first name of the user + * @param lastName the last name of the user + * @param email the email address of the user + * @param passwordHash the hashed password of the user, used for authentication purposes + */ + public User(int userId, String firstName, + String lastName, String email, String passwordHash) { + if (userId <= 0) { + throw new IllegalArgumentException("User ID must be positive"); + } + if (firstName == null || firstName.trim().isEmpty()) { + throw new IllegalArgumentException("First name cannot be null or empty"); + } + if (lastName == null || lastName.trim().isEmpty()) { + throw new IllegalArgumentException("Last name cannot be null or empty"); + } + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("Email cannot be null or empty"); + } + if (passwordHash == null || passwordHash.trim().isEmpty()) { + throw new IllegalArgumentException("Password hash cannot be null or empty"); + } + this.userId = userId; + this.firstName = firstName.trim(); + this.lastName = lastName.trim(); + this.email = email.trim(); + this.passwordHash = passwordHash; +} + + /** + * Gets the unique identifier for the user. + * @return the userId of the user + */ + public int getUserId() { + return userId; + } + + /** + * Gets the role of the user (e.g., "Customer", "Admin"). + * @return the role of the user + */ + public abstract String getRole(); -public class User { + /** + * Gets the first name of the user. + * @return the first name of the user + */ + public String getFirstName() { + return firstName; + } + + /** + * Gets the last name of the user. + * @return the last name of the user + */ + public String getLastName() { + return lastName; + } + + /** + * Gets the email address of the user. + * @return the email of the user + */ + public String getEmail() { + return email; + } + + /** + * Gets the hashed password of the user. + * This is used for authentication purposes and should not be exposed in plaintext. + * @return the password hash of the user + */ + public String getPasswordHash() { + return passwordHash; + } + + /** + * Verifies if the provided password matches the stored password hash. + * This method uses BCrypt to compare the plaintext password with the hashed password. + * @param password the plaintext password to verify + * @return true if the password is correct, false otherwise + * @throws IllegalArgumentException if the password is null, empty, or longer than 72 characters (BCrypt limit) + */ + public boolean verifyPassword(char[] password) { + if (password == null || password.length == 0) { + return false; + } + if (password.length > 72) { // BCrypt has a maximum password length of 72 bytes + throw new IllegalArgumentException("Password cannot be longer than 72 characters"); + } + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + return encoder.matches(new String(password), this.passwordHash); + } } diff --git a/src/main/java/edu/group5/app/model/user/UserRepository.java b/src/main/java/edu/group5/app/model/user/UserRepository.java new file mode 100644 index 0000000..193f433 --- /dev/null +++ b/src/main/java/edu/group5/app/model/user/UserRepository.java @@ -0,0 +1,149 @@ +package edu.group5.app.model.user; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import edu.group5.app.model.DBRepository; + +public class UserRepository extends DBRepository { + public final static String ROLE_CUSTOMER = "Customer"; + + /** + * Constructs UserRepository using Hashmap, + * and extends the content from DBRepository. + * + * @param content the underlying map used to store users, + * where the key represents the user ID + */ + public UserRepository(List rows) { + super(new HashMap<>()); + if (rows == null) { + throw new IllegalArgumentException("The list of rows cannot be null"); + } + for (Object[] row : rows) { + if (row == null || row.length != 6) { + throw new IllegalArgumentException("Each row must contain exactly 6 elements"); + } + int userId = (int) row[0]; + String role = (String) row[1]; + String firstName = (String) row[2]; + String lastName = (String) row[3]; + String email = (String) row[4]; + String passwordHash = (String) row[5]; + + User user; + if (ROLE_CUSTOMER.equalsIgnoreCase(role)) { + user = new Customer(userId, firstName, lastName, email, passwordHash); + } else { + throw new IllegalArgumentException("Unknown role: " + role); + } + this.content.put(userId, user); + } + this.updateContentLock(); + } + + @Override + protected void updateContentLock() { + synchronized (contentLock) { + this.contentLock.clear(); + this.contentLock.putAll(this.content); + } + } + + @Override + public List export() { + Map output = new HashMap<>(this.content); + for (int i : contentLock.keySet()) { + output.remove(i); + } + this.updateContentLock(); + return output.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(entry -> { + User user = entry.getValue(); + return new Object[] { user.getUserId(), user.getRole(), + user.getFirstName(), user.getLastName(), + user.getEmail(), user.getPasswordHash() }; + }) + .toList(); + + } + + public HashMap getUsers() { + return new HashMap<>(content); + } + + /** + * Retrieves a user by their unique identifier. + * + * @param userId the unique identifier of the user to retrieve + * @return the user with the specified ID, or {@code null} if no such user + * exists + * @throws IllegalArgumentException if the userId is not positive + */ + public User getUserById(int userId) { + if (userId <= 0) { + throw new IllegalArgumentException("User ID must be positive"); + } + return content.get(userId); + } + + /** + * Generates the next user ID based on repository size. + * Uses size+1 and then moves forward if that ID is already taken. + * + * @return the next available user ID + * @throws IllegalStateException if no available user ID can be found + */ + public int getNextUserId() { + if (content.isEmpty()) { + return 1; + } + int maxKey = content.keySet().stream().max(Integer::compareTo).orElseThrow( + () -> new IllegalStateException("No keys found")); + int nextId = maxKey + 1; + return nextId; + } /* TODO change this when data database is introduced */ + + /** + * Adds a new user to the repository + *

+ * The user is stored using its {@code userId} as the key. + * If a user with the same ID already exists, the user + * will not be added. + *

+ * + * @param user the user to add + * @return {@code true} if the user was successfully added, and + * {@code false} if a user with the same ID already exists + */ + @Override + public boolean addContent(User user) { + if (user == null) { + throw new IllegalArgumentException("User cannot be null"); + } + if (content.containsKey(user.getUserId())) { + return false; + } + this.content.put(user.getUserId(), user); + return true; + } + + /** + * Finds a user by their email address. + * + * @param email the email address of the user to find + * @return the user with the specified email address, or {@code null} if no such + * user exists + */ + public User findUserByEmail(String email) { + if (email == null || email.trim().isEmpty()) { + throw new IllegalArgumentException("Email cannot be null or empty"); + } + return content.values().stream() + .filter(user -> user.getEmail().equals(email)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/edu/group5/app/model/user/UserService.java b/src/main/java/edu/group5/app/model/user/UserService.java new file mode 100644 index 0000000..628c785 --- /dev/null +++ b/src/main/java/edu/group5/app/model/user/UserService.java @@ -0,0 +1,94 @@ +package edu.group5.app.model.user; + +/** + * Service class for managing user-related operations, such as registration and login. + * It interacts with the UserRepository to perform these operations and contains the business logic + * associated with user management, including validation of input data and handling of user authentication. + */ +public class UserService { + private UserRepository userRepository; + + /** + * Constructs a UserService with the given UserRepository. + * @param userRepository the UserRepository to use for managing user data; must not be null + * @throws IllegalArgumentException if userRepository is null + */ + public UserService(UserRepository userRepository) { + if (userRepository == null) { + throw new IllegalArgumentException("UserRepository cannot be null"); + } + this.userRepository = userRepository; + } + + /** + * Getter for the UserRepository used by this service. + * This method allows access to the user repository for managing user data and performing operations such as registration and login. + * @return the UserRepository instance used by this service + */ + public UserRepository getUserRepository() { + return this.userRepository; + } + + /** + * Registers a new user with the given information. Validates the input data and creates a new User object + * based on the specified role. Currently supports registration for customers only. + * @param role the role of the user (e.g., "Customer"); must not be null or empty + * @param firstName the first name of the user; must not be null or empty + * @param lastName the last name of the user; must not be null or empty + * @param email the email address of the user; must not be null or empty + * @param passwordHash the hashed password of the user; must not be null or empty + * @return true if the user was successfully registered, false if any input is invalid or + * if the role is not supported + * @throws IllegalArgumentException if any of the input parameters are null or empty + * or if the role is not supported + */ + public boolean registerUser(String role, String firstName, String lastName, + String email, String passwordHash) { + if (role == null || role.trim().isEmpty() || + firstName == null || firstName.trim().isEmpty() || + lastName == null || lastName.trim().isEmpty() || + email == null || email.trim().isEmpty() || + passwordHash == null || passwordHash.trim().isEmpty()) { + return false; + } + User user; + if (role.equalsIgnoreCase("Customer")) { + user = new Customer(userRepository.getNextUserId(), firstName, lastName, email, passwordHash); + } else { /* TODO when you switch to a real DB, replace getNextUserId with DB auto-increment/identity and ignore manual ID generation in service*/ + return false; + } + this.userRepository.addContent(user); + return true; + } + + /** + * Authenticates a user based on the provided email and password. + * @param email the email address of the user attempting to log in; must not be null or empty + * @param password the plaintext password of the user attempting to log in; must not be null or empty + * @return the authenticated User object if the login is successful + * (i.e., the user exists and the password is correct), null otherwise + * @throws IllegalArgumentException if email is null or empty, or if password is null or empty + */ + public User login(String email, char[] password) { + if (email == null || email.trim().isEmpty() || password == null || password.length == 0) { + return null; + } + User user = this.userRepository.findUserByEmail(email); + if (user != null && user.verifyPassword(password)) { + return user; + } + return null; + } + + /** + * Retrieves a user by email address. + * @param email the email address of the user to find; must not be null or empty + * @return the User object if found, null otherwise + */ + public User getUserByEmail(String email) { + if (email == null || email.trim().isEmpty()) { + return null; + } + return this.userRepository.findUserByEmail(email); + } +} diff --git a/src/main/java/edu/group5/app/view/Header.java b/src/main/java/edu/group5/app/view/Header.java new file mode 100644 index 0000000..1edc5e4 --- /dev/null +++ b/src/main/java/edu/group5/app/view/Header.java @@ -0,0 +1,78 @@ +package edu.group5.app.view; + +import edu.group5.app.control.HeaderController; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; + +public class Header extends BorderPane { + private final HeaderController controller; + + public Header(HeaderController controller) { + this.controller = controller; + getStylesheets().add(getClass().getResource("/header/header.css").toExternalForm()); + setId("header"); + + setLeft(getLogoSection()); + setCenter(getNavBar()); + setRight(getProfileSection()); + } + + private StackPane getLogoSection() { + StackPane logoSection = new StackPane(); + logoSection.setId("logo-section"); + logoSection.setAlignment(Pos.CENTER); + logoSection.setOnMouseClicked(e -> controller.handleHomeBtn()); + logoSection.setStyle("-fx-cursor: hand;"); + + ImageView logo = new ImageView( + new Image(getClass().getResource("/header/images/hmh-logo.png").toExternalForm()) + ); + logo.setFitHeight(60); + logo.setPreserveRatio(true); + + logoSection.getChildren().add(logo); + return logoSection; + } + + private HBox getNavBar() { + HBox navbar = new HBox(); + navbar.setId("navbar"); + navbar.setAlignment(Pos.CENTER); + navbar.setSpacing(10); + + Button home = new Button("Home"); + home.setOnAction(e -> controller.handleHomeBtn()); + home.setStyle("-fx-cursor: hand;"); + + Button causes = new Button("Causes"); + causes.setOnAction(e -> controller.handleCausesBtn()); + causes.setStyle("-fx-cursor: hand;"); + + Button about = new Button("About us"); + about.setOnAction(e -> controller.handleAboutBtn()); + about.setStyle("-fx-cursor: hand;"); + + navbar.getChildren().addAll(home, causes, about); + return navbar; + } + + private StackPane getProfileSection() { + StackPane profileSection = new StackPane(); + profileSection.setId("profile-section"); + profileSection.setAlignment(Pos.CENTER); + profileSection.setOnMouseClicked(e -> controller.handleProfileBtn()); + profileSection.setStyle("-fx-cursor: hand;"); + + ImageView avatar = new ImageView( + new Image(getClass().getResource("/header/images/avatar.png").toExternalForm()) + ); + avatar.setFitHeight(60); + avatar.setPreserveRatio(true); + + profileSection.getChildren().add(avatar); + return profileSection; + } +} diff --git a/src/main/java/edu/group5/app/view/MainView.java b/src/main/java/edu/group5/app/view/MainView.java new file mode 100644 index 0000000..e35935f --- /dev/null +++ b/src/main/java/edu/group5/app/view/MainView.java @@ -0,0 +1,86 @@ +package edu.group5.app.view; + +import edu.group5.app.control.*; +import edu.group5.app.view.browsepage.BrowsePageView; +import edu.group5.app.control.*; +import edu.group5.app.control.donationpage.DonationPageController; +import edu.group5.app.control.donationpage.PaymentCompleteController; +import edu.group5.app.model.user.User; +import edu.group5.app.model.user.UserService; +import edu.group5.app.view.donationpage.DonationPageView; +import edu.group5.app.view.donationpage.PaymentCompletePageView; +import edu.group5.app.view.homepage.HomePageView; +import edu.group5.app.view.loginpage.LoginPageView; +import edu.group5.app.view.loginpage.SignInPageView; +import edu.group5.app.view.userpage.UserPageView; +import edu.group5.app.view.organizationpage.OrganizationPageView; +import javafx.scene.Scene; +import javafx.scene.layout.BorderPane; + +public class MainView { + private final MainController mainController; + private final HeaderController headerController; + private final HomePageController homePageController; + private final LoginPageController loginPageController; + private final SignInPageController signInPageController; + private final DonationPageController donationPageController; + private final PaymentCompleteController paymentCompleteController; + private final Scene scene; + private final BorderPane root; + + public MainView(MainController mainController, UserService userService) { + this.mainController = mainController; + this.headerController = new HeaderController(mainController); + this.homePageController = new HomePageController(mainController); + this.loginPageController = new LoginPageController(mainController, userService); + this.signInPageController = new SignInPageController(mainController, userService); + this.donationPageController = new DonationPageController(mainController); + this.paymentCompleteController = new PaymentCompleteController(mainController); + this.root = new BorderPane(); + this.scene = new Scene(root, 1280, 720); + } + + public Scene getScene() { + return this.scene; + } + + public Scene createView() { + root.setCenter(new LoginPageView(loginPageController)); + return new Scene(root, 1280, 720); + } + + public void showHomePage(HomePageController homePageController, HeaderController headerController) { + root.setCenter(new HomePageView(homePageController, headerController)); + } + + public void showLoginPage() { + root.setCenter(new LoginPageView(loginPageController)); + } + + public void showBrowsePage(BrowsePageController browsePageController, BrowseCardController browseCardController, HeaderController headerController) { + root.setCenter(new BrowsePageView(getScene(), browsePageController, browseCardController, headerController, mainController)); + } + + public void showOrganizationPage(OrganizationPageController organizationController, HeaderController headerController) { + root.setCenter(new OrganizationPageView(organizationController, headerController, mainController)); + } + + public void showSignInPage() { + root.setCenter(new SignInPageView(signInPageController)); + } + public void showDonationPage() { + root.setCenter(new DonationPageView(donationPageController, headerController, mainController)); + } + public void showPaymentCompletePage() { + root.setCenter(new PaymentCompletePageView(paymentCompleteController)); + } + + public void showAboutUsPage() {} + + public void showUserPage() { + User currentUser = mainController.getCurrentUser(); + if (currentUser != null) { + root.setCenter(new UserPageView(headerController, mainController)); + } + } +} diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java new file mode 100644 index 0000000..ad4be8a --- /dev/null +++ b/src/main/java/edu/group5/app/view/browsepage/BrowseCard.java @@ -0,0 +1,79 @@ +package edu.group5.app.view.browsepage; + +import edu.group5.app.control.BrowseCardController; +import edu.group5.app.model.organization.Organization; +import javafx.geometry.Pos; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public class BrowseCard extends VBox { + private final BrowseCardController controller; + private final Organization organization; + + public BrowseCard(BrowseCardController browseCardController, Organization org, String img) { + this.controller = browseCardController; + this.organization = org; + setId("mainContainer"); + getStylesheets().add(getClass().getResource("/browsepage/browse_org.css").toExternalForm()); + + getChildren().addAll( + imageContainer(img), + orgName(org.name()), + checkMarkContainer() + ); + + setOnMouseClicked(e -> { + controller.handleCardClick(organization); + }); + + setSpacing(10); + setFillWidth(true); + setAlignment(Pos.CENTER); + } + + private StackPane imageContainer(String img) { + StackPane imageContainer = new StackPane(); + imageContainer.setId("imageContainer"); + imageContainer.setPrefHeight(80); + imageContainer.setPrefWidth(80); + imageContainer.setMaxWidth(Double.MAX_VALUE); + + ImageView logo = new ImageView( + new Image(getClass().getResource(img).toExternalForm()) + ); + + logo.setId("logo"); + logo.setSmooth(true); + logo.setPreserveRatio(true); + logo.setFitHeight(80); + + imageContainer.getChildren().add(logo); + return imageContainer; + } + + private Text orgName(String text) { + Text orgName = new Text(text); + orgName.setId("orgName"); + orgName.setWrappingWidth(150); + return orgName; + } + + private StackPane checkMarkContainer() { + StackPane checkMarkContainer = new StackPane(); + checkMarkContainer.setId("checkMarkContainer"); + checkMarkContainer.setAlignment(Pos.CENTER_LEFT); + + ImageView verifiedCheck = new ImageView( + new Image(getClass().getResource("/verified_check.png").toExternalForm()) + ); + + verifiedCheck.setPreserveRatio(true); + verifiedCheck.setSmooth(true); + + checkMarkContainer.getChildren().add(verifiedCheck); + return checkMarkContainer; + } +} diff --git a/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java new file mode 100644 index 0000000..a2d92c8 --- /dev/null +++ b/src/main/java/edu/group5/app/view/browsepage/BrowsePageView.java @@ -0,0 +1,145 @@ +package edu.group5.app.view.browsepage; + +import edu.group5.app.control.BrowseCardController; +import edu.group5.app.control.BrowsePageController; +import edu.group5.app.control.HeaderController; +import edu.group5.app.control.MainController; +import edu.group5.app.model.organization.Organization; +import edu.group5.app.view.Header; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.layout.*; + +import java.util.Map; +import java.util.stream.Collectors; + +public class BrowsePageView extends BorderPane { + private final Scene scene; + private final BrowsePageController controller; + private final BrowseCardController orgController; + private final MainController mainController; + private GridPane organizationGrid; + private Map allOrganizations; + + public BrowsePageView(Scene mainScene, BrowsePageController browsePageController, BrowseCardController browseCardController, HeaderController headerController, MainController mainController) { + this.scene = mainScene; + this.controller = browsePageController; + this.orgController = browseCardController; + this.mainController = mainController; + getStylesheets().add(getClass().getResource("/browsepage/browsepage.css").toExternalForm()); + Header headerView = new Header(headerController); + setTop(headerView); + setCenter(createBody()); + } + + private ScrollPane createBody() { + ScrollPane body = new ScrollPane(); + body.setId("body"); + body.setFitToWidth(true); + body.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + body.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); + + VBox vBox = new VBox(); + vBox.setStyle("-fx-padding: 10;"); + vBox.setSpacing(10); + vBox.setMaxWidth(Double.MAX_VALUE); + vBox.getChildren().addAll( + createSearchSection(), + createOrganizationSection(null) + ); + body.setContent(vBox); + return body; + } + + private HBox createSearchSection() { + HBox searchSection = new HBox(); + TextField searchField = new TextField(); + searchField.setPromptText("Search organizations..."); + + // Add listener for search text changes + searchField.textProperty().addListener((obs, oldVal, newVal) -> { + updateOrganizationGrid(newVal.trim()); + }); + + searchSection.getChildren().add(searchField); + return searchSection; + } + + private GridPane createOrganizationSection(String searchTerm) { + GridPane grid = new GridPane(); + grid.setId("card-grid"); + grid.setHgap(20); + grid.setVgap(20); + grid.setStyle("-fx-padding: 0;"); + grid.setMaxWidth(Double.MAX_VALUE); + + if (allOrganizations == null) { + allOrganizations = mainController.getOrganizationService().getTrustedOrganizations(); + } + + // Filter organizations by search term + Map organizations = filterOrganizations(searchTerm); + + int column = 0; + int row = 0; + + for (Organization org : organizations.values()) { + String defaultImg = "/browsepage/images/children_of_shambala.png"; + BrowseCard card = new BrowseCard(orgController, org, defaultImg); + + grid.add(card, column, row); + + column++; + + if (column == 4) { + column = 0; + row++; + } + } + + for (int i = 0; i < 4; i++) { + ColumnConstraints col = new ColumnConstraints(); + col.setPercentWidth(25); + grid.getColumnConstraints().add(col); + } + + // Store reference for later updates + if (organizationGrid == null) { + organizationGrid = grid; + } + + return grid; + } + + private Map filterOrganizations(String searchTerm) { + // If no search term, return all organizations + if (searchTerm == null || searchTerm.isEmpty()) { + return allOrganizations; + } + + String lowerSearchTerm = searchTerm.toLowerCase(); + return allOrganizations.values().stream() + .filter(org -> org.name().toLowerCase().contains(lowerSearchTerm)) + .collect(Collectors.toMap( + Organization::orgNumber, + org -> org + )); + } + + private void updateOrganizationGrid(String searchTerm) { + if (organizationGrid == null) { + return; + } + + organizationGrid.getChildren().clear(); + organizationGrid.getColumnConstraints().clear(); + + // Rebuild grid with filtered organizations + GridPane updated = createOrganizationSection(searchTerm); + + organizationGrid.getChildren().addAll(updated.getChildren()); + organizationGrid.getColumnConstraints().addAll(updated.getColumnConstraints()); + } +} diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java new file mode 100644 index 0000000..6c5e5e6 --- /dev/null +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -0,0 +1,160 @@ +package edu.group5.app.view.donationpage; + +import edu.group5.app.control.HeaderController; +import edu.group5.app.control.MainController; +import edu.group5.app.control.donationpage.DonationPageController; +import edu.group5.app.view.Header; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.TilePane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextAlignment; +import javafx.scene.Node; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DonationPageView extends BorderPane { + private final DonationPageController controller; + private final MainController mainController; + private final List allDonationElements = new ArrayList<>(); + private final Map elementAmounts = new HashMap<>(); + + public DonationPageView(DonationPageController donationPageController, HeaderController headerController, MainController mainController) { + this.controller = donationPageController; + this.mainController = mainController; + getStylesheets().add(getClass().getResource("/donationpage/donation.css").toExternalForm()); + + Header headerView = new Header(headerController); + setTop(headerView); + + VBox content = new VBox(); + content.getChildren().addAll(createDonationGrid(), createDonateSection()); + setCenter(content); + + } + private TilePane createDonationGrid(){ + TilePane body = new TilePane(); + body.setAlignment(Pos.CENTER); + body.setPrefColumns(3); + body.setPrefRows(2); + body.setPrefTileWidth(300); + body.setPrefTileHeight(150); + body.setHgap(20); + body.setVgap(20); + body.setPadding(new Insets(40, 40, 20, 40)); + + String[][] labels = { + {"Tiny", "50 kr"}, + {"Small", "100 kr"}, + {"Medium", "250 kr"}, + {"Big", "500 kr"}, + {"Grandiose", "1000 kr"}, + }; + for (String[] label : labels) { + body.getChildren().add(createDonationButton(label[0], label[1])); + } + body.getChildren().add(createCustomButton()); + + return body; + } + + public Button createDonationButton(String title, String amount) { + Button button = new Button(title + "\n" + amount); + button.setWrapText(true); + button.setTextAlignment(TextAlignment.CENTER); + button.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + button.getStyleClass().add("donation-button"); + + BigDecimal parsedAmount = parseAmount(amount); + elementAmounts.put(button, parsedAmount); + + button.setOnAction(e -> { + selectDonationElement(button); + }); + allDonationElements.add(button); + return button; + } + private VBox createCustomButton() { + Text titleText = new Text("Custom Donation"); + titleText.getStyleClass().add("donation-title"); + + HBox amountRow = new HBox(4); + amountRow.setAlignment(Pos.CENTER); + + Text krText = new Text("kr"); + krText.getStyleClass().add("donation-amount"); + + TextField amountField = new TextField(); + amountField.getStyleClass().add("donation-input"); + + amountRow.getChildren().addAll(amountField, krText); + + VBox box = new VBox(6, titleText, amountRow); + box.setAlignment(Pos.CENTER); + box.getStyleClass().add("donation-button"); + + + box.setOnMouseClicked(e -> { + try { + BigDecimal amount = new BigDecimal(amountField.getText().trim()); + elementAmounts.put(box, amount); + selectDonationElement(box); + } catch (NumberFormatException exception) { + System.err.println("Invalid custom donation amount: " + amountField.getText()); + } + + }); + + allDonationElements.add(box); + return box; + } + private HBox createDonateSection() { + Button donateBtn = new Button("Donate"); + donateBtn.getStyleClass().add("donate-button"); + donateBtn.setOnAction(e -> controller.handleDonationBtn()); + + HBox section = new HBox(donateBtn); + section.setAlignment(Pos.CENTER); + section.setPadding(new Insets(20, 0, 30, 0)); + return section; + } + + private void selectDonationElement(Node element) { + // Remove selected class from all elements + for (Node node : allDonationElements) { + node.getStyleClass().remove("donation-button-selected"); + } + + element.getStyleClass().add("donation-button-selected"); + + // Extract and store the amount + extractAndStoreAmount(element); + } + + private void extractAndStoreAmount(Node element) { + BigDecimal amount = elementAmounts.get(element); + if (amount != null) { + mainController.setCurrentDonationAmount(amount); + } else { + System.err.println("Error: No amount found for selected element"); + } + } + + private BigDecimal parseAmount(String amountStr) { + try { + return new BigDecimal(amountStr.replace("kr", "").trim()); + } catch (NumberFormatException e) { + return BigDecimal.ZERO; + } + } + +} \ No newline at end of file diff --git a/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java b/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java new file mode 100644 index 0000000..d563fef --- /dev/null +++ b/src/main/java/edu/group5/app/view/donationpage/PaymentCompletePageView.java @@ -0,0 +1,48 @@ +package edu.group5.app.view.donationpage; + +import edu.group5.app.control.donationpage.PaymentCompleteController; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; + +import java.awt.*; +import java.util.Objects; + +public class PaymentCompletePageView extends BorderPane { + private final PaymentCompleteController controller; + + public PaymentCompletePageView(PaymentCompleteController paymentCompleteController) { + this.controller = paymentCompleteController; + getStylesheets().add(getClass().getResource("/donationpage/paymentcomplete.css").toExternalForm()); + + VBox content = new VBox(20); + content.setAlignment(Pos.CENTER); + content.setPadding(new Insets(40)); + content.getChildren().addAll(getImageSection(), getHomeBtn()); + setCenter(content); + } + public VBox getImageSection() { + Image image = new Image(Objects.requireNonNull(getClass().getResourceAsStream("/donationpage/Payment Complete.png"))); + ImageView imageView = new ImageView(image); + imageView.setPreserveRatio(true); + + imageView.fitWidthProperty().bind(widthProperty()); + imageView.setFitHeight(500); + + VBox imageSection = new VBox(imageView); + imageSection.setAlignment(Pos.CENTER); + return imageSection; + } + + public Button getHomeBtn() { + Button home = new Button("Home"); + home.setOnAction(e -> controller.handleHomeBtn()); + home.setId("home-button"); + return home; + } + +} diff --git a/src/main/java/edu/group5/app/view/homepage/HomePageView.java b/src/main/java/edu/group5/app/view/homepage/HomePageView.java new file mode 100644 index 0000000..5510daa --- /dev/null +++ b/src/main/java/edu/group5/app/view/homepage/HomePageView.java @@ -0,0 +1,67 @@ +package edu.group5.app.view.homepage; + +import edu.group5.app.control.HeaderController; +import edu.group5.app.control.HomePageController; +import edu.group5.app.view.Header; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public class HomePageView extends BorderPane { + private final HomePageController controller; + + public HomePageView(HomePageController homePageController, HeaderController headerController) { + this.controller = homePageController; + getStylesheets().add(getClass().getResource("/homepage/homepage.css").toExternalForm()); + Header headerView = new Header(headerController); + setTop(headerView); + setCenter(createBody()); + } + + private ScrollPane createBody() { + ScrollPane body = new ScrollPane(); + body.setFitToWidth(true); + VBox vBox = new VBox(); + vBox.getChildren().addAll( + createIntroductionSection(), + createCharityImageSection() + ); + body.setContent(vBox); + return body; + } + + private VBox createIntroductionSection() { + VBox introductionSection = new VBox(); + introductionSection.setId("introduction-section"); + introductionSection.setAlignment(Pos.CENTER); + introductionSection.setSpacing(10); + + Text h1 = new Text("MAKE A DIFFERENCE TODAY"); + h1.setId("h1"); + Text h2 = new Text("SUPPORT THOSE IN NEED AROUND THE WORLD"); + h2.setId("h2"); + + Button donateToACauseBtn = new Button("Donate to a cause"); + donateToACauseBtn.setOnAction(e -> controller.handleDonateToACauseBtn()); + + Button aboutUsBtn = new Button("About us"); + aboutUsBtn.setOnAction(e -> controller.handleAboutUsBtn()); + + introductionSection.getChildren().addAll(h1, h2, donateToACauseBtn, aboutUsBtn); + return introductionSection; + } + + private StackPane createCharityImageSection() { + StackPane charityImageSection = new StackPane(); + charityImageSection.setId("charity-image-section"); + charityImageSection.setPrefHeight(300); + StackPane image = new StackPane(); + image.setId("charity-image"); + charityImageSection.getChildren().add(image); + return charityImageSection; + } +} diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java new file mode 100644 index 0000000..decd5a1 --- /dev/null +++ b/src/main/java/edu/group5/app/view/loginpage/LoginHeader.java @@ -0,0 +1,33 @@ +package edu.group5.app.view.loginpage; + +import edu.group5.app.control.HeaderController; +import javafx.geometry.Pos; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; + +public class LoginHeader extends BorderPane { + + public LoginHeader() { + getStylesheets().add(getClass().getResource("/header/header.css").toExternalForm()); + setId("header"); + + setCenter(getLogoSection()); + } + private StackPane getLogoSection() { + StackPane logoSection = new StackPane(); + logoSection.setId("logo-section"); + logoSection.setAlignment(Pos.CENTER); + logoSection.setStyle("-fx-cursor: hand;"); + + ImageView logo = new ImageView( + new Image(getClass().getResource("/header/images/hmh-logo.png").toExternalForm()) + ); + logo.setFitHeight(60); + logo.setPreserveRatio(true); + + logoSection.getChildren().add(logo); + return logoSection; + } +} diff --git a/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java new file mode 100644 index 0000000..96d83d7 --- /dev/null +++ b/src/main/java/edu/group5/app/view/loginpage/LoginPageView.java @@ -0,0 +1,114 @@ +package edu.group5.app.view.loginpage; + + +import edu.group5.app.control.HeaderController; +import edu.group5.app.control.LoginPageController; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.*; + +import java.util.Objects; + +public class LoginPageView extends BorderPane { + private final LoginPageController controller; + private TextField emailField; + private PasswordField passwordField; + private Label errorLabel; + + public LoginPageView(LoginPageController loginPageController) { + this.controller = loginPageController; + this.controller.setView(this); + LoginHeader loginHeaderView = new LoginHeader(); + setTop(loginHeaderView); + + HBox content = new HBox(); + content.setFillHeight(true); + HBox.setHgrow(content, Priority.ALWAYS); + + content.getChildren().addAll(getOuterSection(), getImageSection()); + + String css = Objects.requireNonNull( + getClass().getResource("/loginpage/login.css")).toExternalForm(); + content.getStylesheets().add(css); + + setCenter(content); + } + + public String getEmail() { + return emailField.getText(); + } + + public char[] getPassword() { + return passwordField.getText().toCharArray(); + } + + public void showError(String message) { + errorLabel.setText(message); + errorLabel.setStyle("-fx-text-fill: red;"); + } + + private VBox getOuterSection() { + VBox outerSection = new VBox(12); + outerSection.setAlignment(Pos.CENTER); + HBox.setHgrow(outerSection, Priority.ALWAYS); + outerSection.getChildren().addAll(getLoginBox(), getRegisterBtn()); + return outerSection; + } + private VBox getLoginBox() { + VBox loginSection = new VBox(12); + loginSection.setAlignment(Pos.CENTER); + loginSection.setId("login-box"); + loginSection.getChildren().addAll(getErrorLabel(), getEmailBox(), getPasswordBox(), getLoginBtn()); + return loginSection; + } + + private Label getErrorLabel() { + errorLabel = new Label(); + errorLabel.setPrefHeight(20); + return errorLabel; + } + + private VBox getEmailBox() { + VBox emailBox = new VBox(); + emailBox.setMaxWidth(300); + emailField = new TextField(); + emailField.setPromptText("aurafarmer@gmail.com"); + emailField.setMaxWidth(300); + emailBox.getChildren().addAll(new Label("Email"), emailField); + return emailBox; + } + private VBox getPasswordBox() { + VBox passwordBox = new VBox(); + passwordBox.setMaxWidth(300); + passwordField = new PasswordField(); + passwordField.setMaxWidth(300); + passwordBox.getChildren().addAll(new Label("Password"), passwordField); + return passwordBox; + } + private Button getLoginBtn() { + Button loginBtn = new Button("Log In"); + loginBtn.setMaxWidth(300); + loginBtn.setId("login-btn"); + loginBtn.setOnMouseClicked(e -> controller.handleLoginBtn()); + return loginBtn; + } + public Button getRegisterBtn() { + Button registerBtn = new Button("Don't have an account? Sign In"); + registerBtn.setMaxWidth(300); + registerBtn.setOnMouseClicked(e -> controller.handleRegisterBtn()); + registerBtn.setId("register-btn"); + return registerBtn; + } + private StackPane getImageSection() { + StackPane imageSection = new StackPane(); + imageSection.setId("image-section"); + HBox.setHgrow(imageSection, Priority.ALWAYS); + return imageSection; + } + + +} diff --git a/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java new file mode 100644 index 0000000..6ee0e9b --- /dev/null +++ b/src/main/java/edu/group5/app/view/loginpage/SignInPageView.java @@ -0,0 +1,143 @@ +package edu.group5.app.view.loginpage; + +import edu.group5.app.control.HeaderController; +import edu.group5.app.control.SignInPageController; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.*; + +import java.util.Objects; + + +public class SignInPageView extends BorderPane { + private final SignInPageController controller; + private TextField nameField; + private TextField surnameField; + private TextField emailField; + private PasswordField passwordField; + private Label errorLabel; + + public SignInPageView(SignInPageController signInPageController) { + this.controller = signInPageController; + this.controller.setView(this); + setTop(new LoginHeader()); + + HBox content = new HBox(); + content.setFillHeight(true); + HBox.setHgrow(content, Priority.ALWAYS); + + content.getChildren().addAll(getOuterSection(), getImageSection()); + + String css = Objects.requireNonNull( + getClass().getResource("/loginpage/signin.css")).toExternalForm(); + content.getStylesheets().add(css); + + setCenter(content); + + } + + + public String getFirstName() { + return nameField.getText(); + } + + public String getLastName() { + return surnameField.getText(); + } + + public String getEmail() { + return emailField.getText(); + } + + public char[] getPassword() { + return passwordField.getText().toCharArray(); + } + + public void showError(String message) { + errorLabel.setText(message); + errorLabel.setStyle("-fx-text-fill: red;"); + } + + private VBox getOuterSection() { + VBox outerSection = new VBox(12); + outerSection.setAlignment(Pos.CENTER); + HBox.setHgrow(outerSection, Priority.ALWAYS); + outerSection.getChildren().addAll(getSignInBox(), getBackToLoginBtn()); + return outerSection; + } + private VBox getSignInBox() { + VBox signInSection = new VBox(12); + signInSection.setAlignment(Pos.CENTER); + signInSection.setId("login-box"); + signInSection.getChildren().addAll(getErrorLabel(), getNameRow(), getEmailBox(), getPasswordBox(), getSignInBtn()); + return signInSection; + } + + private Label getErrorLabel() { + errorLabel = new Label(); + errorLabel.setPrefHeight(20); + return errorLabel; + } + + private HBox getNameRow() { + HBox nameRow = new HBox(12); + nameRow.setMaxWidth(300); + + VBox nameBox = new VBox(); + nameField = new TextField(); + nameField.setPromptText("Jinwoo"); + HBox.setHgrow(nameBox, Priority.ALWAYS); + nameBox.getChildren().addAll(new Label("First name"), nameField); + + VBox surnameBox = new VBox(); + surnameField = new TextField(); + surnameField.setPromptText("Son"); + HBox.setHgrow(surnameBox, Priority.ALWAYS); + surnameBox.getChildren().addAll(new Label("Last Name"), surnameField); + + nameRow.getChildren().addAll(nameBox, surnameBox); + return nameRow; + + } + private VBox getEmailBox() { + VBox emailBox = new VBox(); + emailBox.setMaxWidth(300); + emailField = new TextField(); + emailField.setPromptText("aurafarmer@gmail.com"); + emailField.setMaxWidth(300); + emailBox.getChildren().addAll(new Label("Email"), emailField); + return emailBox; + } + private VBox getPasswordBox() { + VBox passwordBox = new VBox(); + passwordBox.setMaxWidth(300); + passwordField = new PasswordField(); + passwordField.setMaxWidth(300); + passwordBox.getChildren().addAll(new Label("Password"), passwordField); + return passwordBox; + } + private Button getSignInBtn() { + Button signInBtn = new Button("Sign In"); + signInBtn.setMaxWidth(300); + signInBtn.setId("login-btn"); + signInBtn.setOnMouseClicked(e -> controller.handleSignInBtn()); + return signInBtn; + } + public Button getBackToLoginBtn() { + Button backBtn = new Button("Already have an account? Log in"); + backBtn.setMaxWidth(300); + backBtn.setOnMouseClicked(e -> controller.handleLoginBtn()); + backBtn.setId("register-btn"); + return backBtn; + } + private StackPane getImageSection() { + StackPane imageSection = new StackPane(); + imageSection.setId("image-section"); + HBox.setHgrow(imageSection, Priority.ALWAYS); + return imageSection; + } + +} diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java new file mode 100644 index 0000000..4ec7c91 --- /dev/null +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -0,0 +1,103 @@ +package edu.group5.app.view.organizationpage; + +import edu.group5.app.control.HeaderController; +import edu.group5.app.control.MainController; +import edu.group5.app.control.OrganizationPageController; +import edu.group5.app.model.organization.Organization; +import edu.group5.app.view.Header; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +public class OrganizationPageView extends BorderPane { + private final OrganizationPageController controller; + private final MainController mainController; + + public OrganizationPageView(OrganizationPageController controller, HeaderController headerController, MainController mainController) { + this.controller = controller; + this.mainController = mainController; + getStylesheets().add(getClass().getResource("/organizationpage/organizationpage.css").toExternalForm()); + Header headerView = new Header(headerController); + setTop(headerView); + setCenter(createBody()); + } + + private ScrollPane createBody() { + ScrollPane body = new ScrollPane(); + body.setFitToWidth(true); + body.setFitToHeight(true) + ; + VBox vBox = new VBox(); + vBox.setId("main-container"); + + vBox.getChildren().addAll( + createOrgSection() + ); + body.setContent(vBox); + return body; + } + + private HBox createOrgSection() { + HBox orgSection = new HBox(); + orgSection.setId("org-section"); + orgSection.setAlignment(Pos.CENTER); + orgSection.setSpacing(40); + + orgSection.getChildren().addAll(createImageContainer(), createOrgInfoSection()); + return orgSection; + } + + private StackPane createImageContainer() { + StackPane imageContainer = new StackPane(); + imageContainer.setId("imageContainer"); + imageContainer.setPrefHeight(120); + imageContainer.setPrefWidth(120); + imageContainer.setMaxWidth(Double.MAX_VALUE); + + Organization org = mainController.getCurrentOrganization(); + String imagePath = org != null ? "/browsepage/images/children_of_shambala.png" : "/browsepage/images/children_of_shambala.png"; + + ImageView logo = new ImageView( + new Image(getClass().getResource(imagePath).toExternalForm()) + ); + + logo.setId("logo"); + logo.setSmooth(true); + logo.setPreserveRatio(true); + + imageContainer.getChildren().add(logo); + return imageContainer; + } + + private VBox createOrgInfoSection() { + Organization org = mainController.getCurrentOrganization(); + + VBox orgInfoSection = new VBox(); + orgInfoSection.setSpacing(50); + + VBox orgNameAndDescription = new VBox(); + + Label orgName = new Label(org != null ? org.name() : "Unknown Organization"); + orgName.setId("orgName"); + + Text description = new Text(org != null ? org.description() : "No description available"); + description.setId("description"); + + orgNameAndDescription.getChildren().addAll(orgName, description); + + Button donateBtn = new Button("Donate"); + donateBtn.setId("donate-button"); + donateBtn.setOnAction(e -> controller.handleDonateClick()); + + orgInfoSection.getChildren().addAll(orgNameAndDescription, donateBtn); + return orgInfoSection; + } +} diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java new file mode 100644 index 0000000..9ab73ee --- /dev/null +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -0,0 +1,136 @@ +package edu.group5.app.view.userpage; + +import edu.group5.app.control.HeaderController; +import edu.group5.app.control.MainController; +import edu.group5.app.model.donation.Donation; +import edu.group5.app.model.organization.Organization; +import edu.group5.app.model.user.User; +import edu.group5.app.view.Header; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + + +public class UserPageView extends BorderPane { + private final User currentUser; + private final MainController mainController; + + public UserPageView(HeaderController headerController, MainController mainController) { + this.mainController = mainController; + this.currentUser = mainController.getCurrentUser(); + + getStylesheets().add(getClass().getResource("/userpage/userpage.css").toExternalForm()); + + Header headerView = new Header(headerController); + setTop(headerView); + + VBox content = new VBox(30); + content.setPadding(new Insets(40)); + content.getChildren().addAll(createProfileSection(), createCausesSection(), createDonationsSection()); + setCenter(content); + } + + private HBox createProfileSection() { + ImageView avatar = new ImageView(new Image(getClass().getResourceAsStream("/userpage/account_circle.png"))); + avatar.setFitWidth(150); + avatar.setFitHeight(150); + avatar.setPreserveRatio(true); + avatar.setId("avatar"); + + Text name = new Text(currentUser.getFirstName() + " " + currentUser.getLastName()); + name.setId("profile-name"); + + Label email = new Label(currentUser.getEmail()); + email.getStyleClass().add("profile-info"); + + Label location = new Label("Trondheim, Norway"); + location.getStyleClass().add("profile-info"); + + Button logoutBtn = new Button("Logout"); + logoutBtn.getStyleClass().add("logout-button"); + logoutBtn.setOnAction(e -> mainController.logout()); + + VBox info = new VBox(10, name, email, location, logoutBtn); + info.setAlignment(Pos.CENTER_LEFT); + + HBox profile = new HBox(40, avatar, info); + profile.setAlignment(Pos.CENTER_LEFT); + return profile; + } + + private VBox createCausesSection() { + Text title = new Text("YOUR SUPPORTED CAUSES"); + title.getStyleClass().add("section-title"); + + VBox causesBox = new VBox(10); + causesBox.getStyleClass().add("section-box"); + causesBox.setPadding(new Insets(10)); + + HashMap userDonations = mainController.getDonationService() + .getDonationRepository().filterByUser(currentUser.getUserId()); + + Set uniqueOrgs = new HashSet<>(); + for (Donation donation : userDonations.values()) { + uniqueOrgs.add(donation.organizationId()); + } + + if (uniqueOrgs.isEmpty()) { + Label noCauses = new Label("No causes supported yet"); + noCauses.setStyle("-fx-text-fill: #999;"); + causesBox.getChildren().add(noCauses); + } else { + for (int orgId : uniqueOrgs) { + Organization org = mainController.getOrganizationService().findByOrgNumber(orgId); + if (org != null) { + Label causeLabel = new Label("• " + org.name()); + causesBox.getChildren().add(causeLabel); + } + } + } + + return new VBox(10, title, causesBox); + } + + private VBox createDonationsSection() { + Text title = new Text("PREVIOUS DONATIONS"); + title.getStyleClass().add("section-title"); + + VBox donationsBox = new VBox(10); + donationsBox.getStyleClass().add("section-box"); + donationsBox.setPadding(new Insets(10)); + + HashMap userDonations = mainController.getDonationService() + .getDonationRepository().filterByUser(currentUser.getUserId()); + + if (userDonations.isEmpty()) { + Label noDonations = new Label("No donations yet"); + noDonations.setStyle("-fx-text-fill: #999;"); + donationsBox.getChildren().add(noDonations); + } else { + for (Donation donation : userDonations.values()) { + Organization org = mainController.getOrganizationService() + .findByOrgNumber(donation.organizationId()); + String orgName = (org != null) ? org.name() : "Unknown Organization"; + + Label donationLabel = new Label( + orgName + " • " + donation.amount() + " kr" + " • " + donation.date() + ); + donationsBox.getChildren().add(donationLabel); + } + } + + return new VBox(10, title, donationsBox); + } + +} diff --git a/src/main/resources/browsepage/browse_org.css b/src/main/resources/browsepage/browse_org.css new file mode 100644 index 0000000..9a50c71 --- /dev/null +++ b/src/main/resources/browsepage/browse_org.css @@ -0,0 +1,25 @@ +#mainContainer { + -fx-border-color: black; + -fx-border-width: 1px; + -fx-border-radius: 1em; + -fx-padding: 5px; + -fx-background-color: white; + -fx-background-radius: 1em; +} + +#mainContainer:hover { + -fx-cursor: hand; +} + +#imageContainer {} + + +#logo {} + +#orgName { + -fx-font-size: x-large; + -fx-font-weight: bold; + -fx-text-alignment: center; +} + +#checkMarkContainer {} \ No newline at end of file diff --git a/src/main/resources/browsepage/browsepage.css b/src/main/resources/browsepage/browsepage.css new file mode 100644 index 0000000..1dd8643 --- /dev/null +++ b/src/main/resources/browsepage/browsepage.css @@ -0,0 +1,6 @@ +#body { + -fx-padding: 20px; +} + +#card-grid { +} \ No newline at end of file diff --git a/src/main/resources/browsepage/images/children_of_shambala.png b/src/main/resources/browsepage/images/children_of_shambala.png new file mode 100644 index 0000000..3b1a7f7 Binary files /dev/null and b/src/main/resources/browsepage/images/children_of_shambala.png differ diff --git a/src/main/resources/browsepage/images/kfum_kfum_global.png b/src/main/resources/browsepage/images/kfum_kfum_global.png new file mode 100644 index 0000000..bd5365a Binary files /dev/null and b/src/main/resources/browsepage/images/kfum_kfum_global.png differ diff --git a/src/main/resources/donationpage/Payment Complete.png b/src/main/resources/donationpage/Payment Complete.png new file mode 100644 index 0000000..e2b3d6a Binary files /dev/null and b/src/main/resources/donationpage/Payment Complete.png differ diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css new file mode 100644 index 0000000..32433df --- /dev/null +++ b/src/main/resources/donationpage/donation.css @@ -0,0 +1,53 @@ +.donation-button { + -fx-background-color: white; + -fx-border-color: #ccc; + -fx-border-width: 2px; + -fx-border-radius: 8; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-font-size: 18px; + -fx-font-weight: bold; +} + +.donation-button:hover { + -fx-border-color: #f0f0f0; +} + +.donation-button-selected { + -fx-background-color: #111; + -fx-text-fill: white; + -fx-border-color: #111; +} +.donation-title { + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-fill: #111; +} +.donation-amount { + -fx-font-size: 18px; + -fx-fill: #111; +} +.donation-input { + -fx-font-size:16px; + -fx-pref-width: 140px; + -fx-background-color: transparent; + -fx-border-color: transparent transparent #333 transparent; + -fx-alignment: center; +} +.donation-input:focused { + -fx-border-color: transparent transparent #4a90d9 transparent; +} +.donate-button { + -fx-pref-height: 55px; + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 40 0 40; +} +.donate-button:hover { + -fx-background-color: #c02020; +} + diff --git a/src/main/resources/donationpage/paymentcomplete.css b/src/main/resources/donationpage/paymentcomplete.css new file mode 100644 index 0000000..0e1e786 --- /dev/null +++ b/src/main/resources/donationpage/paymentcomplete.css @@ -0,0 +1,14 @@ +#home-button { + -fx-background-color: #4caf50; + -fx-text-fill: white; + -fx-font-size: 18px; + -fx-font-weight: bold; + -fx-pref-width: 200px; + -fx-pref-height: 50px; + -fx-background-radius: 8; + -fx-cursor: hand; +} + +#home-button:hover { + -fx-background-color: #388e3c; +} \ No newline at end of file diff --git a/src/main/resources/header/header.css b/src/main/resources/header/header.css new file mode 100644 index 0000000..28fcc59 --- /dev/null +++ b/src/main/resources/header/header.css @@ -0,0 +1,19 @@ +#header { + -fx-background-color: #A6CDF2; + -fx-padding: 10px; +} + +#logo-section { + -fx-border-color: black; + -fx-border-width: 2px; +} + +#navbar { + -fx-border-color: black; + -fx-border-width: 2px; +} + +#profile-section { + -fx-border-color: black; + -fx-border-width: 2px; +} \ No newline at end of file diff --git a/src/main/resources/header/images/avatar.png b/src/main/resources/header/images/avatar.png new file mode 100644 index 0000000..e46b74d Binary files /dev/null and b/src/main/resources/header/images/avatar.png differ diff --git a/src/main/resources/header/images/hmh-logo.png b/src/main/resources/header/images/hmh-logo.png new file mode 100644 index 0000000..78be8d9 Binary files /dev/null and b/src/main/resources/header/images/hmh-logo.png differ diff --git a/src/main/resources/homepage/homepage.css b/src/main/resources/homepage/homepage.css new file mode 100644 index 0000000..90b090c --- /dev/null +++ b/src/main/resources/homepage/homepage.css @@ -0,0 +1,27 @@ +#introduction-section { + -fx-border-color: black; + -fx-border-width: 2px; + -fx-padding: 20px 0; +} + +#h1 { + -fx-text-fill: black; + -fx-font-size: 50pt; + -fx-font-weight: bold; +} + +#h2 { + -fx-fill: #757575; + -fx-font-size: 25pt; +} + +#charity-image-section { + -fx-border-color: black; +} + +#charity-image { + -fx-background-image: url("/homepage/images/charityimage.jpg"); + -fx-background-position: center 55%; + -fx-background-size: 100% auto; + -fx-background-repeat: no-repeat; +} \ No newline at end of file diff --git a/src/main/resources/homepage/images/charityimage.jpg b/src/main/resources/homepage/images/charityimage.jpg new file mode 100644 index 0000000..9072612 Binary files /dev/null and b/src/main/resources/homepage/images/charityimage.jpg differ diff --git a/src/main/resources/init.sql b/src/main/resources/init.sql new file mode 100644 index 0000000..8b99314 --- /dev/null +++ b/src/main/resources/init.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS +users ( + user_id INT PRIMARY KEY, + role VARCHAR(32) NOT NULL, + first_name VARCHAR(32) NOT NULL, + last_name VARCHAR(32) NOT NULL, + email VARCHAR(32) NOT NULL, + password_hash VARCHAR(72) NOT NULL +); + +CREATE TABLE IF NOT EXISTS +donations ( + donation_id INT PRIMARY KEY, + user_id INT NOT NULL, + organization_id INT NOT NULL, + amount DECIMAL(32, 16) NOT NULL, + dating TIMESTAMP NOT NULL, + payment_method VARCHAR(32) NOT NULL, + CONSTRAINT fk_user + FOREIGN KEY (user_id) + REFERENCES users(user_id) +); + diff --git a/src/main/resources/loginpage/login-image.jpg b/src/main/resources/loginpage/login-image.jpg new file mode 100644 index 0000000..5b4b3ed Binary files /dev/null and b/src/main/resources/loginpage/login-image.jpg differ diff --git a/src/main/resources/loginpage/login.css b/src/main/resources/loginpage/login.css new file mode 100644 index 0000000..dc758a1 --- /dev/null +++ b/src/main/resources/loginpage/login.css @@ -0,0 +1,25 @@ +#image-section { + -fx-background-image: url("/loginpage/login-image.jpg"); + -fx-background-size: 200%; + -fx-background-position: left center; + -fx-background-repeat: no-repeat; + -fx-pref-width: 50%; +} +#login-btn { + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; +} + +#register-btn { + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; +} +#login-box { + -fx-border-color: #ccc; + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-padding: 24px; + -fx-max-width: 340px; +} \ No newline at end of file diff --git a/src/main/resources/loginpage/signin-image.png b/src/main/resources/loginpage/signin-image.png new file mode 100644 index 0000000..7cb78cd Binary files /dev/null and b/src/main/resources/loginpage/signin-image.png differ diff --git a/src/main/resources/loginpage/signin.css b/src/main/resources/loginpage/signin.css new file mode 100644 index 0000000..4ab0276 --- /dev/null +++ b/src/main/resources/loginpage/signin.css @@ -0,0 +1,25 @@ +#image-section { + -fx-background-image: url("/loginpage/signin-image.png"); + -fx-background-size: auto; + -fx-background-position: right center; + -fx-background-repeat: no-repeat; + -fx-pref-width: 50%; +} +#login-btn { + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; +} + +#register-btn { + -fx-background-color: #000000; + -fx-text-fill: white; + -fx-pref-height: 35px; +} +#login-box { + -fx-border-color: #ccc; + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-padding: 24px; + -fx-max-width: 340px; +} \ No newline at end of file diff --git a/src/main/resources/organizationpage/organizationpage.css b/src/main/resources/organizationpage/organizationpage.css new file mode 100644 index 0000000..a7276b5 --- /dev/null +++ b/src/main/resources/organizationpage/organizationpage.css @@ -0,0 +1,31 @@ +#main-container { + -fx-padding: 50px +} + +#logo { + -fx-min-height: 50%; +} + +#orgName { + -fx-font-weight: bold; + -fx-font-size: 20pt; +} + +#description { + -fx-font-size: 10pt; +} + +#donate-button { + -fx-pref-height: 55px; + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 22px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 40 0 40; +} + +#donate-button:hover { + -fx-background-color: #c02020; +} \ No newline at end of file diff --git a/src/main/resources/test_init.sql b/src/main/resources/test_init.sql new file mode 100644 index 0000000..00670e8 --- /dev/null +++ b/src/main/resources/test_init.sql @@ -0,0 +1,24 @@ +DROP TABLE IF EXISTS users, donations; + +CREATE TABLE IF NOT EXISTS +users ( + user_id INT PRIMARY KEY, + role VARCHAR(32) NOT NULL, + first_name VARCHAR(32) NOT NULL, + last_name VARCHAR(32) NOT NULL, + email VARCHAR(32) NOT NULL, + password_hash VARCHAR(72) NOT NULL +); + +CREATE TABLE IF NOT EXISTS +donations ( + donation_id INT PRIMARY KEY, + user_id INT NOT NULL, + organization_id INT NOT NULL, + amount DECIMAL(32, 16) NOT NULL, + dating TIMESTAMP NOT NULL, + payment_method VARCHAR(32) NOT NULL, + CONSTRAINT fk_user + FOREIGN KEY (user_id) + REFERENCES users(user_id) +); diff --git a/src/main/resources/userpage/account_circle.png b/src/main/resources/userpage/account_circle.png new file mode 100644 index 0000000..f53f825 Binary files /dev/null and b/src/main/resources/userpage/account_circle.png differ diff --git a/src/main/resources/userpage/userpage.css b/src/main/resources/userpage/userpage.css new file mode 100644 index 0000000..8401a77 --- /dev/null +++ b/src/main/resources/userpage/userpage.css @@ -0,0 +1,31 @@ +#profile-name { + -fx-font-size: 28px; + -fx-font-weight: bold; +} +.profile-info { + -fx-font-size: 16px; + -fx-text-fill: #444; +} +.section-title { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-fill: #888; +} +.section-box { + -fx-background-color: #ddd; + -fx-pref-height: 120px; + -fx-pref-width: 700px; + -fx-background-radius: 6; +} +.logout-button { + -fx-background-color: #e03030; + -fx-text-fill: white; + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-padding: 8 20; + -fx-background-radius: 4; + -fx-cursor: hand; +} +.logout-button:hover { + -fx-background-color: #c02020; +} \ No newline at end of file diff --git a/src/main/resources/verified_check.png b/src/main/resources/verified_check.png new file mode 100644 index 0000000..520f237 Binary files /dev/null and b/src/main/resources/verified_check.png differ diff --git a/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java b/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java new file mode 100644 index 0000000..8c401c4 --- /dev/null +++ b/src/test/java/edu/group5/app/control/wrapper/DbWrapperDonationsTest.java @@ -0,0 +1,207 @@ +package edu.group5.app.control.wrapper; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class DbWrapperDonationsTest { + private Object[] johnDonation; + private List users; + private Object[] janeDonation; + private Object[] cutoffDonation; + private Object[] freakyDonation; + private Object[] repeatingDonation; + private List donations; + private List donations2; + private List donations3; + private List repeatedDonations; + private List wrongFormatDonations; + private List wrongDatatypeDonations; + private List nullList; + + private static final int PRECISION = 5; + + private DbWrapper db; + + @BeforeEach + void init() { + this.db = new DbWrapper(true); + String[] firstNames = new String[] { "John", "Jane", "Cutoff", "Freaky", "Repeating" }; + String[] lastNames = new String[] { "Doe", "Doe", "Joh", "Bill", "JoeJoe" }; + this.users = new ArrayList(); + for (int i = 0; i < 5; i++) { + Object[] row = new Object[6]; + row[0] = i + 1; + row[1] = "Customer"; + row[2] = firstNames[i]; + row[3] = lastNames[i]; + row[4] = firstNames[i] + lastNames[i] + "@email.com"; + row[5] = "password"; + users.add(row); + } + + this.johnDonation = new Object[] { 1, 1, 39, new BigDecimal(20.02), new Timestamp(new Date().getTime()), + "Paypal" }; + this.donations = new ArrayList(); + this.donations.add(this.johnDonation); + + this.janeDonation = new Object[] { 2, 2, 39, new BigDecimal(20.00), new Timestamp(new Date().getTime()), + "Visa debit card" }; + this.donations2 = new ArrayList(); + this.donations2.add(this.johnDonation); + this.donations2.add(this.janeDonation); + + this.donations3 = new ArrayList(); + this.donations3.add(this.janeDonation); + + this.repeatingDonation = new Object[] { 3, 3, 333, new BigDecimal(18181818.18), + new Timestamp(new Date().getTime()), "Klarna installations payment" }; + this.repeatedDonations = new ArrayList(); + this.repeatedDonations.add(this.repeatingDonation); + this.repeatedDonations.add(this.repeatingDonation); + + this.cutoffDonation = new Object[] { 4, 4, 21 }; + this.wrongFormatDonations = new ArrayList(); + this.wrongFormatDonations.add(cutoffDonation); + + this.freakyDonation = new Object[] { 5, 5, "Freaks4Education", "lots", false, new String[6] }; + this.wrongDatatypeDonations = new ArrayList(); + this.wrongDatatypeDonations.add(freakyDonation); + + Object[] nullRow = new Object[] {null, null, null, null, null, null}; + this.nullList = new ArrayList(); + this.nullList.add(nullRow); + + this.db.connect(); + this.db.exportUsers(users); + } + + private static boolean donationEquals(Object[] array1, Object[] array2) { + Object[] tempArray1 = array1.clone(); + tempArray1[3] = ((BigDecimal) tempArray1[3]).setScale(2, RoundingMode.HALF_UP); + Object[] tempArray2 = array2.clone(); + tempArray2[3] = ((BigDecimal) tempArray2[3]).setScale(2, RoundingMode.HALF_UP); + return Arrays.equals(tempArray1, tempArray2); + } + + @Test + public void importDonationsIsOnlyExportDonationsTest() { + assertDoesNotThrow(() -> { + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); + assertTrue(this.db.exportDonations(this.donations) == 1); + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 1); + assertTrue(donationEquals(this.donations.get(0), this.db.importDonations((int) this.users.get(0)[0]).get(0))); + assertTrue(this.db.disconnect()); + }); + } + + @Test + public void nullDataInExportDonationsThrowsExpectedException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(null); + }); + assertTrue(this.db.disconnect()); + assertEquals("data can't be null", exception.getMessage()); + } + + @Test + public void wronglyFormattedDonationsThrowsExpectedException() { + assertTrue(this.db.importDonations((int) this.users.get(2)[0]).size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(this.wrongFormatDonations); + }); + assertTrue(this.db.importDonations((int) this.users.get(2)[0]).size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("data's arrays must have a length of 6", exception.getMessage()); + } + + @Test + public void wronglyDatatypedDonationsThrowsExpectedException() { + assertTrue(this.db.importDonations((int) this.users.get(3)[0]).size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(this.wrongDatatypeDonations); + }); + assertTrue(this.db.importDonations((int) this.users.get(3)[0]).size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("One or more rows in data contains a wrong datatype", exception.getMessage()); + } + + @Test + public void addingSameDonationTwiceThrowsExpectedException() { + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); + assertEquals(1, this.db.exportDonations(this.donations)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(this.donations); + }); + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 1); + assertTrue(this.db.disconnect()); + assertEquals("data can't contain existing rows", exception.getMessage()); + } + + @Test + public void addingSameDonationTwiceThrowsExpectedException2() { + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); + assertTrue(this.db.importDonations((int) this.users.get(1)[0]).size() == 0); + assertEquals(2, this.db.exportDonations(this.donations2)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(this.donations); + }); + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 1); + assertTrue(this.db.importDonations((int) this.users.get(1)[0]).size() == 1); + assertTrue(this.db.disconnect()); + assertEquals("data can't contain existing rows", exception.getMessage()); + } + + @Test + public void addingDifferentDonationsThrowsNoException() { + assertDoesNotThrow(() -> { + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); + assertTrue(this.db.importDonations((int) this.users.get(1)[0]).size() == 0); + assertEquals(1, this.db.exportDonations(this.donations)); + assertEquals(1, this.db.exportDonations(this.donations3)); + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 1); + assertTrue(this.db.importDonations((int) this.users.get(1)[0]).size() == 1); + assertTrue(this.db.disconnect()); + }); + } + + @Test + public void addingDonationListWithDuplicateIdsThrowsExpectedException() { + assertTrue(this.db.importDonations((int) this.users.get(4)[0]).size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(this.repeatedDonations); + }); + assertTrue(this.db.importDonations((int) this.users.get(4)[0]).size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("data can't contain duplicate rows", exception.getMessage()); + } + + @Test + public void addingDonationListWithNullInRowThrowsExpectedException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportDonations(this.nullList); + }); + assertTrue(this.db.disconnect()); + assertEquals("One or more rows in data contains null values", exception.getMessage()); + } + + @Test + public void dataIsEmptyAfterExportingAndImportingEmptyList() { + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); + assertEquals(0, this.db.exportDonations(new ArrayList())); + assertTrue(this.db.importDonations((int) this.users.get(0)[0]).size() == 0); + assertTrue(this.db.disconnect()); + } +} diff --git a/src/test/java/edu/group5/app/control/wrapper/DbWrapperUserTest.java b/src/test/java/edu/group5/app/control/wrapper/DbWrapperUserTest.java new file mode 100644 index 0000000..10c514f --- /dev/null +++ b/src/test/java/edu/group5/app/control/wrapper/DbWrapperUserTest.java @@ -0,0 +1,191 @@ +package edu.group5.app.control.wrapper; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.util.Date; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import javafx.util.converter.BigDecimalStringConverter; + +public class DbWrapperUserTest { + private Object[] johnDoe; + private Object[] janeDoe; + private Object[] cutoffJoh; + private Object[] freakyBill; + private Object[] repeatingJoeJoe; + private List users; + private List users2; + private List users3; + private List repeatedUsers; + private List wrongFormatUsers; + private List wrongDatatypeUsers; + private List nullList; + + private DbWrapper db; + + @BeforeEach + void init() { + this.db = new DbWrapper(true); + this.johnDoe = new Object[] { 1, "Customer", "John", "Doe", "johndoe@email.com", "password" }; + this.users = new ArrayList(); + this.users.add(this.johnDoe); + + this.janeDoe = new Object[] { 2, "Customer", "Jane", "Doe", "janedoe@email.com", "qwerty" }; + this.users2 = new ArrayList(); + this.users2.add(this.johnDoe); + this.users2.add(this.janeDoe); + + this.users3 = new ArrayList(); + this.users3.add(this.janeDoe); + + this.repeatingJoeJoe = new Object[] { 3, "Customer", "Repeating", "JoeJoe", "repeatingjjoe@email.com", + "passwordpassword" }; + this.repeatedUsers = new ArrayList(); + this.repeatedUsers.add(this.repeatingJoeJoe); + this.repeatedUsers.add(this.repeatingJoeJoe); + + this.cutoffJoh = new Object[] { 4, "Customer", "Cutoff", "Joh" }; + this.wrongFormatUsers = new ArrayList(); + this.wrongFormatUsers.add(cutoffJoh); + + this.freakyBill = new Object[] { 5, "Customer", 6.1805011125, "Bill", true, false }; + this.wrongDatatypeUsers = new ArrayList(); + this.wrongDatatypeUsers.add(freakyBill); + + Object[] nullRow = new Object[] {null, null, null, null, null, null}; + this.nullList = new ArrayList(); + this.nullList.add(nullRow); + } + + @Test + public void nonTestDbWrapperThrowsNoException() { + assertDoesNotThrow(() -> new DbWrapper(false)); + } + + @Test + public void importUsersIsOnlyExportUsersTest() { + assertDoesNotThrow(() -> { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.exportUsers(this.users) == 1); + assertTrue(this.db.importUsers().size() == 1); + assertTrue(Arrays.equals(this.db.importUsers().get(0), this.johnDoe)); + assertTrue(this.db.disconnect()); + }); + } + + @Test + public void nullDataInExportUsersThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(null); + }); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("data can't be null", exception.getMessage()); + } + + @Test + public void wronglyFormattedUsersThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.wrongFormatUsers); + }); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("data's arrays must have a length of 6", exception.getMessage()); + } + + @Test + public void wronglyDatatypedUsersThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.wrongDatatypeUsers); + }); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("One or more rows in data contains a wrong datatype", exception.getMessage()); + } + + @Test + public void addingSameUserTwiceThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + assertEquals(1, this.db.exportUsers(this.users)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.users); + }); + assertTrue(this.db.importUsers().size() == 1); + assertTrue(this.db.disconnect()); + assertEquals("data can't contain existing rows", exception.getMessage()); + } + + @Test + public void addingSameUserTwiceThrowsExpectedException2() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + assertEquals(2, this.db.exportUsers(this.users2)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.users); + }); + assertTrue(this.db.importUsers().size() == 2); + assertTrue(this.db.disconnect()); + assertEquals("data can't contain existing rows", exception.getMessage()); + } + + @Test + public void addingDifferentUsersThrowsNoException() { + assertDoesNotThrow(() -> { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + assertEquals(1, this.db.exportUsers(this.users)); + assertEquals(1, this.db.exportUsers(this.users3)); + assertTrue(this.db.importUsers().size() == 2); + assertTrue(this.db.disconnect()); + }); + } + + @Test + public void addingUserListWithDuplicateIdsThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.repeatedUsers); + }); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("data can't contain duplicate rows", exception.getMessage()); + } + + @Test + public void addingUserListWithNullInRowThrowsExpectedException() { + assertTrue(this.db.connect()); + assertTrue(this.db.importUsers().size() == 0); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + this.db.exportUsers(this.nullList); + }); + assertTrue(this.db.importUsers().size() == 0); + assertTrue(this.db.disconnect()); + assertEquals("One or more rows in data contains null values", exception.getMessage()); + } + + +} diff --git a/src/test/java/edu/group5/app/control/wrapper/OrgApiWrapperTest.java b/src/test/java/edu/group5/app/control/wrapper/OrgApiWrapperTest.java new file mode 100644 index 0000000..23b30ec --- /dev/null +++ b/src/test/java/edu/group5/app/control/wrapper/OrgApiWrapperTest.java @@ -0,0 +1,101 @@ +package edu.group5.app.control.wrapper; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.IllegalArgumentException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.jackson.core.exc.StreamReadException; + +/** + * A test class for the OrgApiWrapper class. + */ +public class OrgApiWrapperTest { + String testUrl; + String wrongUrl; + String wrongUrl2; + + /** + * Initiates the urlStrings used for testing. + */ + @BeforeEach + void init() { + this.testUrl = "https://app.innsamlingskontrollen.no/api/public/v1/all"; + this.wrongUrl = "This is not a URL"; + this.wrongUrl2 = "https://www.google.com"; + } + + /** + * Checks if inputting null as the urlString throws the expected exception + * during construction. + */ + @Test + public void nullUrlThrowsException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new OrgApiWrapper(null)); + assertEquals("url can't be null", exception.getMessage()); + } + + /** + * Checks if inputting an empty urlString throws the expected exception during + * construction. + */ + @Test + public void emptyUrlThrowsException() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> new OrgApiWrapper("")); + assertEquals("url can't be blank", exception.getMessage()); + } + + /** + * Checks if an invalid urlString throws the expected exception during + * construction. + */ + @Test + public void faultyUrlThrowsException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> new OrgApiWrapper(this.wrongUrl)); + assertEquals("url has to be valid", exception.getMessage()); + } + + // /** + // * Checks if import returns False when there's no internet connection. + // */ + // @Test + // public void noConnectionReturnsFalseImport() { + // assertDoesNotThrow(() -> { + // OrgAPIWrapper api = new OrgAPIWrapper(this.testUrl); + // assertFalse(api.importData()); + // }); + // } + + /** + * Checks if import actually imports something. + */ + @Test + public void importsNonEmptyData() { + assertDoesNotThrow(() -> { + OrgApiWrapper api = new OrgApiWrapper(testUrl); + assertTrue(api.importData()); + assertFalse(api.getData().length == 0); + }); + } + + /** + * Checks if an unparseable website throws the expected exception. + */ + @Test + public void nonParseableSiteThrowsExceptionWhenImporting() { + assertDoesNotThrow(() -> { + OrgApiWrapper api = new OrgApiWrapper(this.wrongUrl2); + StreamReadException exception = assertThrows( + StreamReadException.class, () -> api.importData()); + assertEquals("The URL leads to a website that can't be parsed", exception.getMessage()); + }); + } + +} diff --git a/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java b/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java new file mode 100644 index 0000000..ce81b2a --- /dev/null +++ b/src/test/java/edu/group5/app/model/donation/DonationRepositoryTest.java @@ -0,0 +1,275 @@ +package edu.group5.app.model.donation; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class DonationRepositoryTest { + + private DonationRepository repo; + private Donation donation1, donation2, donation3, donation4; + private Timestamp ts1, ts2, now; + + @BeforeEach + void setUp() { + ts1 = Timestamp.valueOf("2025-01-01 10:00:00"); + ts2 = Timestamp.valueOf("2025-01-01 10:00:01"); + now = new Timestamp(System.currentTimeMillis()); + + donation1 = new Donation(1, 102, 202, new BigDecimal("500.0"), ts1, "Card"); + donation2 = new Donation(2, 102, 202, new BigDecimal("500.0"), ts2, "PayPal"); + donation3 = new Donation(3, 103, 203, new BigDecimal("200.0"), now, "PayPal"); + donation4 = new Donation(1, 101, 201, new BigDecimal("500.0"), now, "Card"); + + repo = new DonationRepository(new ArrayList<>()); + } + + @Test + void constructorThrowsIfNullList() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new DonationRepository(null)); + assertEquals("The list of rows cannot be null", ex.getMessage()); + } + + @Test + void constructorThrowsIfRowInvalidLength() { + List invalidRows = new ArrayList<>(); + invalidRows.add(new Object[]{1, 2}); + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new DonationRepository(invalidRows)); + assertEquals("Each row must contain exactly 6 elements", ex.getMessage()); + } + + @Test + void constructorParsesRowSuccessfully() { + List rows = new ArrayList<>(); + Timestamp ts = Timestamp.valueOf("2025-01-01 10:00:00"); + rows.add(new Object[]{1, 101, 201, new BigDecimal("100"), ts, "Card"}); + DonationRepository repoWithRow = new DonationRepository(rows); + Donation d = repoWithRow.getDonationById(1); + assertNotNull(d); + assertEquals(101, d.userId()); + assertEquals(201, d.organizationId()); + assertEquals(new BigDecimal("100"), d.amount()); + assertEquals(ts, d.date()); + assertEquals("Card", d.paymentMethod()); + } + + + @Test + void constructorThrowsIfRowIsNull() { + List rows = new ArrayList<>(); + rows.add(null); + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new DonationRepository(rows)); + assertEquals("Each row must contain exactly 6 elements", ex.getMessage()); + } + + @Test + void addContentSuccessfully() { + assertTrue(repo.addContent(donation1)); + assertEquals(1, repo.getAllDonations().size()); + assertTrue(repo.getAllDonations().containsValue(donation1)); + } + + @Test + void addContentDuplicateIdFails() { + repo.addContent(donation1); + assertFalse(repo.addContent(donation4)); + assertEquals(1, repo.getAllDonations().size()); + } + + @Test + void addContentNullThrows() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> repo.addContent(null)); + assertEquals("Donation cannot be null", ex.getMessage()); + } + + @Test + void getDonationByIdSuccessfully() { + repo.addContent(donation1); + Donation retrieved = repo.getDonationById(1); + assertEquals(donation1, retrieved); + } + + @Test + void getDonationByIdThrowsIfNegative() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> repo.getDonationById(0)); + assertEquals("Donation ID must be positive", ex.getMessage()); + } + + @Test + void getDonationByIdReturnsNullIfNotFound() { + assertNull(repo.getDonationById(999)); + } + + @Test + void getAllDonationsReturnsCopy() { + repo.addContent(donation1); + Map all = repo.getAllDonations(); + assertEquals(1, all.size()); + all.clear(); + assertEquals(1, repo.getAllDonations().size()); + } + + @Test + void sortByDateAscending() { + repo.addContent(donation3); + repo.addContent(donation1); + repo.addContent(donation2); + + Map sorted = repo.sortByDate(); + Integer[] keys = sorted.keySet().toArray(new Integer[0]); + assertArrayEquals(new Integer[]{1, 2, 3}, keys); + } + + @Test + void sortByDateWithSameDateKeepsAll() { + repo.addContent(donation1); + repo.addContent(donation4); + Map sorted = repo.sortByDate(); + assertEquals(1, sorted.size()); + assertTrue(sorted.containsKey(1)); + } + + @Test + void sortByAmountAscending() { + repo.addContent(donation3); + repo.addContent(donation1); + + Map sorted = repo.sortByAmount(); + Integer[] keys = sorted.keySet().toArray(new Integer[0]); + assertArrayEquals(new Integer[]{3, 1}, keys); + } + + @Test + void sortByAmountWithSameAmount() { + repo.addContent(donation1); + repo.addContent(donation2); + Map sorted = repo.sortByAmount(); + assertEquals(2, sorted.size()); + assertTrue(sorted.containsKey(1)); + assertTrue(sorted.containsKey(2)); + } + + @Test + void filterByOrganizationMatches() { + repo.addContent(donation1); + repo.addContent(donation2); + repo.addContent(donation3); + + Map filtered = repo.filterByOrganization(202); + assertEquals(2, filtered.size()); + assertTrue(filtered.containsKey(1)); + assertTrue(filtered.containsKey(2)); + } + + @Test + void filterByOrganizationNoMatch() { + repo.addContent(donation1); + Map filtered = repo.filterByOrganization(999); + assertTrue(filtered.isEmpty()); + } + + @Test + void filterByOrganizationThrowsIfNegative() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> repo.filterByOrganization(0)); + assertEquals("Organization number must be positive", ex.getMessage()); + } + + @Test + void exportReturnsAllRowsSortedById() { + repo.addContent(donation2); + repo.addContent(donation1); + + List exported = repo.export(); + assertEquals(2, exported.size()); + assertArrayEquals(new Object[]{1, 102, 202, new BigDecimal("500.0"), ts1, "Card"}, exported.get(0)); + assertArrayEquals(new Object[]{2, 102, 202, new BigDecimal("500.0"), ts2, "PayPal"}, exported.get(1)); + } + + @Test + void getNextDonationIdReturnsCorrectNextId() { + assertEquals(1, repo.getNextDonationId()); + repo.addContent(donation1); + repo.addContent(donation4); + assertEquals(2, repo.getNextDonationId()); + } + + @Test + void exportEmptyRepositoryReturnsEmptyList() { + List exported = repo.export(); + assertTrue(exported.isEmpty()); + } + + @Test + void sortByDateHandlesDuplicateKeysWithLambda() { + Donation d1 = new Donation(1, 101, 201, new BigDecimal("100"), ts1, "Card"); + Donation d2 = new Donation(1, 102, 202, new BigDecimal("200"), ts2, "PayPal"); + repo.addContent(d1); + repo.getAllDonations().put(d2.donationId(), d2); + Map sorted = repo.sortByDate(); + assertEquals(d1, sorted.get(1)); + } + + @Test + void sortByAmountHandlesDuplicateKeysWithLambda() { + Donation d1 = new Donation(1, 101, 201, new BigDecimal("100"), ts1, "Card"); + Donation d2 = new Donation(1, 102, 202, new BigDecimal("100"), ts2, "PayPal"); + repo.addContent(d1); + repo.getAllDonations().put(d2.donationId(), d2); + Map sorted = repo.sortByAmount(); + assertEquals(d1, sorted.get(1)); + } + + @Test + void filterByOrganizationHandlesDuplicateKeysWithLambda() { + Donation d1 = new Donation(1, 101, 201, new BigDecimal("100"), ts1, "Card"); + Donation d2 = new Donation(1, 102, 201, new BigDecimal("200"), ts2, "PayPal"); + repo.addContent(d1); + repo.getAllDonations().put(d2.donationId(), d2); + Map filtered = repo.filterByOrganization(201); + assertEquals(d1, filtered.get(1)); + } + + @Test + void filterByUserIdMatches() { + repo.addContent(donation1); + repo.addContent(donation3); + + Map filtered = repo.filterByUser(102); + assertEquals(1, filtered.size()); + assertTrue(filtered.containsKey(1)); + } + + @Test + void filterByUserIdNoMatch() { + repo.addContent(donation1); + Map filtered = repo.filterByUser(999); + assertTrue(filtered.isEmpty()); + } + + @Test + void filterByUserIdThrowsIfNegative() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> repo.filterByUser(0)); + assertEquals("User ID must be positive", ex.getMessage()); + } + + @Test + void exportExportsOnlyNewInformation() { + repo.addContent(donation1); + repo.addContent(donation2); + assertEquals(2, repo.export().size()); + repo.addContent(donation3); + assertEquals(1, repo.export().size()); + } +} \ No newline at end of file diff --git a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java new file mode 100644 index 0000000..80d37bf --- /dev/null +++ b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java @@ -0,0 +1,125 @@ +package edu.group5.app.model.donation; + +import edu.group5.app.model.organization.OrganizationRepository; +import edu.group5.app.model.user.Customer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; + + +import static org.junit.jupiter.api.Assertions.*; + +class DonationServiceTest { + + private DonationRepository donationRepository; + private OrganizationRepository organizationRepository; + private DonationService donationService; + private Customer customer; + + @BeforeEach + void setUp() { + HashMap orgMap = new HashMap<>(); + orgMap.put("org_number", "101"); + orgMap.put("name", "CharityOrg"); + orgMap.put("status", "approved"); + orgMap.put("url", "https://charity.org"); + orgMap.put("is_pre_approved", true); + + Object[] orgInput = new Object[]{ orgMap }; + organizationRepository = new OrganizationRepository(orgInput); + + donationRepository = new DonationRepository(new ArrayList<>()); + + + donationService = new DonationService(donationRepository, organizationRepository); + + customer = new Customer(1, "John", "Doe", "john@example.com", "$2a$10$hashed"); + } + + @Test + void testConstructorThrowsIfDonationRepositoryIsNull() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new DonationService(null, organizationRepository); + }); + assertEquals("DonationRepository cannot be null", exception.getMessage()); + } + + @Test + void testConstructorThrowsIfOrganizationRepositoryIsNull() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + new DonationService(donationRepository, null); + }); + assertEquals("OrganizationRepository cannot be null", exception.getMessage()); + } + + @Test + void testGetDonationRepository() { + assertEquals(donationRepository, donationService.getDonationRepository()); + } + + @Test + void testGetOrganizationRepository() { + assertEquals(organizationRepository, donationService.getOrganizationRepository()); + } + + @Test + void donateReturnsFalseIfCustomerNull() { + boolean result = donationService.donate(null, + 101, BigDecimal.TEN, "Card"); + assertFalse(result); + } + + @Test + void donateReturnsFalseIfAmountInvalid() { + assertFalse(donationService.donate(customer, + 101, null, "Card")); + assertFalse(donationService.donate(customer, + 101, BigDecimal.ZERO, "Card")); + assertFalse(donationService.donate(customer, + 101, new BigDecimal("-5"), "Card")); + } + + @Test + void donateReturnsFalseIfPaymentMethodBlank() { + assertFalse(donationService.donate(customer, + 101, BigDecimal.TEN, "")); + assertFalse(donationService.donate(customer, + 101, BigDecimal.TEN, " ")); + } + + @Test + void donateReturnsFalseIfOrganizationNotFound() { + boolean result = donationService.donate(customer, 999, BigDecimal.TEN, "Card"); + assertFalse(result); + } + + @Test + void testDonateAddsDonationSuccessfully() { + BigDecimal amount = new BigDecimal("50.00"); + String paymentMethod = "PayPal"; + boolean result = donationService.donate(customer, 101, amount, paymentMethod); + assertTrue(result); + assertEquals(1, donationRepository.getAllDonations().size()); + + Donation donation = donationRepository.getAllDonations().values().iterator().next(); + assertEquals(customer.getUserId(), donation.userId()); + assertEquals(101, donation.organizationId()); + assertEquals(amount, donation.amount()); + assertEquals(paymentMethod, donation.paymentMethod()); + assertTrue(donation.date().before(Timestamp.from(Instant.now())) || + donation.date().equals(Timestamp.from(Instant.now()))); + } + + @Test + void testDonateWithMultipleDonations() { + donationService.donate(customer, 101, new BigDecimal("10.00"), "PayPal"); + donationService.donate(customer, 101, new BigDecimal("25.00"), "Card"); + assertEquals(2, donationRepository.getAllDonations().size()); + } +} \ No newline at end of file diff --git a/src/test/java/edu/group5/app/model/donation/DonationTest.java b/src/test/java/edu/group5/app/model/donation/DonationTest.java new file mode 100644 index 0000000..f8f9069 --- /dev/null +++ b/src/test/java/edu/group5/app/model/donation/DonationTest.java @@ -0,0 +1,99 @@ +package edu.group5.app.model.donation; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.sql.Timestamp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DonationTest { + + private String expectedMessage; + private int donationId1; + private int userId1; + private int organizationId1; + private BigDecimal amount1; + private Timestamp date1; + private String paymentMethod1; + private Donation donation; + + @BeforeEach + void setUp(){ + donationId1 = 1; + userId1 = 101; + organizationId1 = 202; + amount1 = new BigDecimal("500.0"); + date1 = new Timestamp(System.currentTimeMillis()); + paymentMethod1 = "Card"; + donation = new Donation(donationId1, userId1, + organizationId1, amount1, date1, paymentMethod1); + + } + + private void exceptionTest(int donationId, int userId, + int organizationId, BigDecimal amount, + Timestamp date, String paymentMethod) { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, () -> new Donation( + donationId, userId, organizationId, amount, date, paymentMethod) + ); + assertEquals(expectedMessage, exception.getMessage()); + } + + @Test + void testIfDonationGetsDonationValues() { + + assertEquals(1, donation.donationId()); + assertEquals(101, donation.userId()); + assertEquals(202, donation.organizationId()); + assertEquals(new BigDecimal("500.0"), donation.amount()); + assertEquals(date1, donation.date()); + assertEquals("Card", donation.paymentMethod()); + } + + @Test + void testIfThrowsExceptionWhenDonationIdIsNotPositive() { + expectedMessage = "Donation ID must be positive"; + exceptionTest(0, userId1, organizationId1, + amount1, date1, paymentMethod1); + } + + @Test + void testIfThrowsExceptionWhenUserIdIsNotPositive() { + expectedMessage = "User ID must be positive"; + exceptionTest(donationId1, -1, organizationId1, + amount1, date1, paymentMethod1); + } + @Test + void testIfThrowsExceptionWhenOrganizationIdIsNotPositive() { + expectedMessage = "Organization ID must be positive"; + exceptionTest(donationId1, userId1, 0, + amount1, date1, paymentMethod1); + } + @Test + void testIfThrowsExceptionWhenAmountIsNotPositive() { + expectedMessage = "Amount must be positive and not null"; + exceptionTest(donationId1, userId1, organizationId1, + new BigDecimal("0.00"), date1, paymentMethod1); + } + @Test + void testIfThrowsExceptionWhenDateIsNull() { + expectedMessage = "Date must not be null"; + exceptionTest(donationId1, userId1, organizationId1, + amount1, null, paymentMethod1); + } + @Test + void testIfThrowsExceptionWhenPaymentMethodIsNullOrEmpty() { + expectedMessage = "Payment method must not be empty"; + exceptionTest(donationId1, userId1, organizationId1, + amount1, date1, null); + exceptionTest(donationId1, userId1, organizationId1, + amount1, date1, " "); + + } + + +} diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java new file mode 100644 index 0000000..7a5ece5 --- /dev/null +++ b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java @@ -0,0 +1,137 @@ +package edu.group5.app.model.organization; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class OrganizationRepositoryTest { + + private OrganizationRepository repository; + + @BeforeEach + void setUp() { + Object[] content = new Object[] { + Map.of( + "org_number", "1", + "name", "Trusted Org1", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ), + Map.of( + "org_number", "2", + "name", "Trusted Org2", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ), + Map.of( + "org_number", "3", + "name", "Untrusted Org1", + "status", "pending", + "url", "org.com", + "is_pre_approved", true + ), + Map.of( + "org_number", "4", + "name", "Untrusted Org2", + "status", "pending", + "url", "org.com", + "is_pre_approved", true + ) + }; + repository = new OrganizationRepository(content); + } + + private void constructorTest(Object[] input, String expectedMessage) { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new OrganizationRepository(input) + ); + assertEquals(expectedMessage, exception.getMessage()); + } + @Test + void constructor_ThrowsWhenContentIsNull() { + constructorTest(null, "The input cannot be null"); + } + + @Test + void getTrustedOrganizations_OnlyReturnsTrustedOrganizations() { + Map trusted = repository.getTrustedOrganizations(); + + assertEquals(2, trusted.size()); + assertTrue(trusted.containsKey(1)); + assertTrue(trusted.containsKey(2)); + assertFalse(trusted.containsKey(3)); + assertFalse(trusted.containsKey(4)); + assertTrue(trusted.values().stream().allMatch(Organization::trusted)); + } + + @Test + void testFindByOrgNumberReturnsOrganization() { + assertEquals(new Organization(1, "Trusted Org1", true, + "org.com", true, "Information about Trusted Org1"), + repository.findByOrgNumber(1)); + } + + @Test + void testFindByOrgNumberIfOrgNumberIsIllegal() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> repository.findByOrgNumber(-1)); + assertEquals("The Organization number must be a positive integer", exception.getMessage()); + } + + @Test + void testFindByOrgNumberIfOrgNumberNotFound() { + assertNull(repository.findByOrgNumber(999)); + } + + @Test + void testFindByOrgNameReturnsOrganization() { + assertEquals(new Organization(1, "Trusted Org1", true, + "org.com", true, "Information about Trusted Org1"), + repository.findByOrgName("Trusted Org1")); + } + + @Test + void testFindByOrgNameIfNameIsIllegalThrowsException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> repository.findByOrgName(null)); + assertEquals("The name cannot be null", exception.getMessage()); + + IllegalArgumentException exception2 = assertThrows(IllegalArgumentException.class, + () -> repository.findByOrgName("")); + assertEquals("The name cannot be null", exception2.getMessage()); + } + + @Test + void testFindByOrgNameIfNameNotFound() { + assertNull(repository.findByOrgName("Nonexistent Org")); + } + + @Test + void testFindByOrgNameIsCaseInsensitive() { + assertEquals(new Organization(1, "Trusted Org1", true, + "org.com", true, "Information about Trusted Org1"), + repository.findByOrgName("trusted org1")); + } + + @Test + void testExportAllOrganizations() { + Object[] allOrgs = repository.export(); + assertEquals(4, allOrgs.length); + } + + @Test + void testExportAllOrganizationsThrowsWhenRepositoryIsEmpty() { + OrganizationRepository emptyRepo = new OrganizationRepository(new Object[0]); + IllegalStateException exception = assertThrows( + IllegalStateException.class, () -> emptyRepo.export() + ); + assertEquals("The repository is empty", exception.getMessage()); + } +} \ No newline at end of file diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java new file mode 100644 index 0000000..e34aba7 --- /dev/null +++ b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java @@ -0,0 +1,70 @@ +package edu.group5.app.model.organization; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +public class OrganizationServiceTest { + private OrganizationRepository repo; + private OrganizationService service; + private Object[] content; + + @BeforeEach + public void setUp() { + Map orgMap = new HashMap<>(); + orgMap.put("org_number", "1"); + orgMap.put("name", "Misjonsalliansen"); + orgMap.put("status", "approved"); + orgMap.put("url", "https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/"); + orgMap.put("is_pre_approved", false); + + content = new Object[]{orgMap}; + repo = new OrganizationRepository(content); + service = new OrganizationService(repo); + } + + @Test + void constructor_throwsIfNull() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new OrganizationService(null)); + assertEquals("OrganizationRepository cannot be null", ex.getMessage()); + } + + @Test + void testGetOrganizationRepository() { + assertEquals(repo, service.getOrganizationRepository()); + } + + @Test + void testGetTrustedOrganizations() { + Map trustedOrgs = service.getTrustedOrganizations(); + assertNotNull(trustedOrgs); + assertTrue(trustedOrgs.containsKey(1)); + Organization org = trustedOrgs.get(1); + assertEquals(1, org.orgNumber()); + assertEquals("Misjonsalliansen", org.name()); + assertTrue(org.trusted()); + assertEquals("https://www.innsamlingskontrollen.no/organisasjoner/misjonsalliansen/", org.websiteUrl()); + assertFalse(org.isPreApproved()); + } + + @Test + void testFindByOrgNumber() { + Organization org = service.findByOrgNumber(1); + assertNotNull(org); + assertEquals(1, org.orgNumber()); + assertEquals("Misjonsalliansen", org.name()); + } + + + @Test + void testFindByOrgName() { + Organization org = service.findByOrgName("Misjonsalliansen"); + assertNotNull(org); + assertEquals(1, org.orgNumber()); + assertEquals("Misjonsalliansen", org.name()); + } +} diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationTest.java new file mode 100644 index 0000000..f921b60 --- /dev/null +++ b/src/test/java/edu/group5/app/model/organization/OrganizationTest.java @@ -0,0 +1,101 @@ +package edu.group5.app.model.organization; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class OrganizationTest { + + @Test + void constructor_CreatesAnOrganizationWhenInputIsValid() { + Organization org = new Organization( + 1, + "Org", + true, + "org.com", + true, + "Org description" + ); + + assertAll( + () -> assertEquals(1, org.orgNumber()), + () -> assertEquals("Org", org.name()), + () -> assertTrue(org.trusted()), + () -> assertEquals("org.com", org.websiteUrl()), + () -> assertTrue(org.isPreApproved()), + () -> assertEquals("Org description", org.description()) + ); + } + + @Test + void constructor_ThrowsWhenOrgNumberIsNegative() { + assertThrows(IllegalArgumentException.class, () -> new Organization( + -1, + "Org", + true, + "org.com", + true, + "Org description" + )); + } + + @Test + void constructor_ThrowsWhenNameIsNull() { + assertThrows(NullPointerException.class, () -> new Organization( + 1, + null, + true, + "org.com", + true, + "Org description" + )); + } + + @Test + void constructor_ThrowsWhenNameIsBlank() { + assertThrows(IllegalArgumentException.class, () -> new Organization( + 1, + "", + true, + "org.com", + true, + "Org description" + )); + } + + @Test + void constructor_ThrowsWhenWebsiteURLIsNull() { + assertThrows(NullPointerException.class, () -> new Organization( + 1, + "Org", + true, + null, + true, + "Org description" + )); + } + + @Test + void constructor_ThrowsWhenWebsiteURLIsBlank() { + assertThrows(IllegalArgumentException.class, () -> new Organization( + 1, + "Org", + true, + "", + true, + "Org description" + )); + } + + @Test + void constructor_ThrowsWhenDescriptionIsNull() { + assertThrows(NullPointerException.class, () -> new Organization( + 1, + "Org", + true, + "org.com", + true, + null + )); + } +} \ No newline at end of file diff --git a/src/test/java/edu/group5/app/model/user/CustomerTest.java b/src/test/java/edu/group5/app/model/user/CustomerTest.java new file mode 100644 index 0000000..7c96438 --- /dev/null +++ b/src/test/java/edu/group5/app/model/user/CustomerTest.java @@ -0,0 +1,221 @@ +package edu.group5.app.model.user; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +public class CustomerTest { + + private int testUserId; + private String testFirstName; + private String testLastName; + private String testEmail; + private String testPassword; + private String testPasswordHash; + + @BeforeEach + void setUp() { + testUserId = 1; + testFirstName = "John"; + testLastName = "Doe"; + testEmail = "john.doe@example.com"; + testPassword = "password123"; + testPasswordHash = new BCryptPasswordEncoder().encode(testPassword); + } + + private void constructorTest(int userId, String firstName, + String lastName, String email, String passwordHash, + String expectedMessage) { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new Customer(userId, firstName, lastName, email, passwordHash) + ); + + assertEquals(expectedMessage, exception.getMessage()); + } + + @Test + void constructorCreatesValidUser() { + User user = new Customer(testUserId, testFirstName, + testLastName, testEmail, testPasswordHash); + assertEquals(testUserId, user.getUserId()); + assertEquals(testFirstName, user.getFirstName()); + assertEquals(testLastName, user.getLastName()); + assertEquals(testEmail, user.getEmail()); + assertEquals(testPasswordHash, user.getPasswordHash()); + } + + @Test + void testInstanceOfCustomer() { + User user = new Customer(testUserId, testFirstName, + testLastName, testEmail, testPasswordHash); + assertTrue(user instanceof Customer); + } + + @Test + void constructorWithNegativeUserIdThrowsException() { + constructorTest(-1, testFirstName, testLastName, + testEmail, testPasswordHash, "User ID must be positive"); + } + + @Test + void constructorWithNullFirstNameThrowsException() { + constructorTest(testUserId, null, testLastName, + testEmail, testPasswordHash, "First name cannot be null or empty"); + } + + @Test + void constructorWithEmptyFirstNameThrowsException() { + constructorTest(testUserId, "", testLastName, + testEmail, testPasswordHash, "First name cannot be null or empty"); + } + + @Test + void constructorWithNullLastNameThrowsException() { + constructorTest(testUserId, testFirstName, null, + testEmail, testPasswordHash, "Last name cannot be null or empty"); + } + + @Test + void constructorWithEmptyLastNameThrowsException() { + constructorTest(testUserId, testFirstName, + "", testEmail, testPasswordHash, "Last name cannot be null or empty"); + } + + @Test + void constructorWithNullEmailThrowsException() { + constructorTest(testUserId, testFirstName, testLastName, + null, testPasswordHash, "Email cannot be null or empty"); + } + + @Test + void constructorWithEmptyEmailThrowsException() { + constructorTest(testUserId, testFirstName, testLastName, + "", testPasswordHash, "Email cannot be null or empty"); + } + + @Test + void constructorWithNullPasswordHashThrowsException() { + constructorTest(testUserId, testFirstName, testLastName, + testEmail, null, "Password hash cannot be null or empty"); + } + + @Test + void constructorWithEmptyPasswordHashThrowsException() { + constructorTest(testUserId, testFirstName, testLastName, + testEmail, "", "Password hash cannot be null or empty"); + } + + + @Test + void verifyPasswordReturnsTrueForCorrectPassword() { + User user = new Customer(testUserId, testFirstName, + testLastName, testEmail, testPasswordHash); + + assertTrue(user.verifyPassword(testPassword.toCharArray())); + } + + @Test + void verifyPasswordReturnsFalseForIncorrectPassword() { + User user = new Customer(testUserId, testFirstName, + testLastName, testEmail, testPasswordHash); + + assertFalse(user.verifyPassword("wrongPassword".toCharArray())); + } + + @Test + void verifyPasswordReturnsFalseForNullPassword() { + User user = new Customer(testUserId, testFirstName, + testLastName, testEmail, testPasswordHash); + + assertFalse(user.verifyPassword(null)); + } + + @Test + void verifyPasswordReturnsFalseForEmptyPassword() { + User user = new Customer(testUserId, testFirstName, + testLastName, testEmail, testPasswordHash); + + assertFalse(user.verifyPassword("".toCharArray())); + } + + @Test + void verifyPasswordThrowsExceptionForTooLongPassword() { + User user = new Customer(testUserId, testFirstName, + testLastName, testEmail, testPasswordHash); + char[] longPassword = new char[73]; // 73 characters, exceeding BCrypt limit + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> user.verifyPassword(longPassword) + ); + assertEquals("Password cannot be longer than 72 characters", exception.getMessage()); + } + + @Test + void getRoleReturnsCustomer() { + User user = new Customer(testUserId, testFirstName, + testLastName, testEmail, testPasswordHash); + assertEquals(UserRepository.ROLE_CUSTOMER, user.getRole()); + } + + @Test + void addPreferenceAddsOrganizationNumber() { + Customer customer = new Customer(testUserId, testFirstName, testLastName, testEmail, testPasswordHash); + customer.addPreference(123); + assertTrue(customer.getPreferences().contains(123)); + } + + @Test + void addPreferenceWithNegativeOrgNumberThrowsException() { + Customer customer = new Customer(testUserId, testFirstName, testLastName, testEmail, testPasswordHash); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> customer.addPreference(-1) + ); + assertEquals("Organization number must be a positive integer", exception.getMessage()); + } + + @Test + void addPreferenceWithExistingOrgNumberThrowsException() { + Customer customer = new Customer(testUserId, testFirstName, testLastName, testEmail, testPasswordHash); + customer.addPreference(123); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> customer.addPreference(123) + ); + assertEquals("Organization number already in preferences", exception.getMessage()); + } + + @Test + void removePreferenceRemovesOrganizationNumber() { + Customer customer = new Customer(testUserId, testFirstName, testLastName, testEmail, testPasswordHash); + customer.addPreference(123); + customer.removePreference(123); + assertFalse(customer.getPreferences().contains(123)); + } + + @Test + void removePreferenceWithNegativeOrgNumberThrowsException() { + Customer customer = new Customer(testUserId, testFirstName, testLastName, testEmail, testPasswordHash); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> customer.removePreference(-1) + ); + assertEquals("Organization number must be a positive integer", exception.getMessage()); + } + + @Test + void removePreferenceWithNonExistingOrgNumberThrowsException() { + Customer customer = new Customer(testUserId, testFirstName, testLastName, testEmail, testPasswordHash); + customer.addPreference(123); + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> customer.removePreference(456) + ); + assertEquals("Organization number not found in preferences", exception.getMessage()); + } +} diff --git a/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java b/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java new file mode 100644 index 0000000..eaf2c2c --- /dev/null +++ b/src/test/java/edu/group5/app/model/user/UserRepositoryTest.java @@ -0,0 +1,148 @@ +package edu.group5.app.model.user; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class UserRepositoryTest { + +private UserRepository repo; +private List rows; +private User additionalUser; + + @BeforeEach + void setUp() { + rows = new ArrayList<>(); + rows.add(new Object[]{1, "Customer", "John", "Cena", "john@example.com", "hashedpass"}); + rows.add(new Object[]{2, "Customer", "Jane", "Doe", "jane@example.com", "hashedpass"}); + repo = new UserRepository(rows); + this.additionalUser = new Customer(3, "John", "Doe", "john@example.com", "hashedpass"); + } + + @Test + void constructorThrowsIfNull() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new UserRepository(null)); + assertEquals("The list of rows cannot be null", ex.getMessage()); + } + + @Test + void constructorThrowsIfInvalidRowLength() { + List invalid = new ArrayList<>(); + invalid.add(new Object[]{1, "Customer"}); + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new UserRepository(invalid)); + assertEquals("Each row must contain exactly 6 elements", ex.getMessage()); + } + + @Test + void constructorThrowsIfUnknownRole() { + List badRole = new ArrayList<>(); + badRole.add(new Object[]{3, "Admin", "Bob", "Smith", "bob@example.com", "pass"}); + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new UserRepository(badRole)); + assertEquals("Unknown role: Admin", ex.getMessage()); + } + + @Test + void constructorThrowsIfRowNull() { + List invalid = new ArrayList<>(); + invalid.add(null); + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new UserRepository(invalid)); + assertEquals("Each row must contain exactly 6 elements", ex.getMessage()); + } + + @Test + void constructorThrowsIfRowHasIncorrectLength() { + List invalid = new ArrayList<>(); + invalid.add(new Object[]{1, "Customer", "John"}); + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new UserRepository(invalid)); + assertEquals("Each row must contain exactly 6 elements", ex.getMessage()); + } + + @Test + void addContentSuccessfully() { + Customer user = new Customer(3, "Bob", "Smith", "bob@example.com", "pass"); + assertTrue(repo.addContent(user)); + assertEquals(3, repo.getUsers().size()); + } + + @Test + void addContentNullThrows() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> repo.addContent(null)); + assertEquals("User cannot be null", ex.getMessage()); + } + + @Test + void addContentDuplicateReturnsFalse() { + Customer user = new Customer(1, "John", "Cena", "john@example.com", "pass"); + assertFalse(repo.addContent(user)); + } + + @Test + void findUserByEmailSuccessfully() { + User u = repo.findUserByEmail("jane@example.com"); + assertEquals("Jane", u.getFirstName()); + } + + @Test + void findUserByEmailReturnsNullIfNotFound() { + assertNull(repo.findUserByEmail("notfound@example.com")); + } + + @Test + void findUserByEmailThrowsIfNullOrEmpty() { + IllegalArgumentException ex1 = assertThrows(IllegalArgumentException.class, + () -> repo.findUserByEmail(null)); + assertEquals("Email cannot be null or empty", ex1.getMessage()); + + IllegalArgumentException ex2 = assertThrows(IllegalArgumentException.class, + () -> repo.findUserByEmail(" ")); + assertEquals("Email cannot be null or empty", ex2.getMessage()); + } + +@Test +void getUserByIdSuccessfully() { + User u = repo.getUserById(1); + assertEquals("John", u.getFirstName()); + } + +@Test +void getUserByIdReturnsNullIfNotFound() { + assertNull(repo.getUserById(999)); + } + + @Test + void getUserByIdThrowsIfNonPositive() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> repo.getUserById(0)); + assertEquals("User ID must be positive", ex.getMessage()); + } + + + @Test + void getNextUserIdReturnsNextAvailable() { + int nextId = repo.getNextUserId(); + assertEquals(3, nextId); + } + + @Test + void getNextUserIdReturns1IfEmpty() { + UserRepository emptyRepo = new UserRepository(new ArrayList<>()); + assertEquals(1, emptyRepo.getNextUserId()); + } + + @Test + void exportExportsOnlyNewInformation() { + assertEquals(0, repo.export().size()); + repo.addContent(this.additionalUser); + assertEquals(1, repo.export().size()); + } +} \ No newline at end of file diff --git a/src/test/java/edu/group5/app/model/user/UserServiceTest.java b/src/test/java/edu/group5/app/model/user/UserServiceTest.java new file mode 100644 index 0000000..acec10b --- /dev/null +++ b/src/test/java/edu/group5/app/model/user/UserServiceTest.java @@ -0,0 +1,168 @@ +package edu.group5.app.model.user; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class UserServiceTest { + private UserRepository repo; + private UserService service; + + @BeforeEach + void setUp() { + List rows = new ArrayList<>(); + rows.add(new Object[]{1, "Customer", "John", "Cena", "john.cena@example.com", "$2a$10$hashed"}); + rows.add(new Object[]{2, "Customer", "Jane", "Doe", "jane.doe@example.com", "$2a$10$hashed"}); + repo = new UserRepository(rows); + service = new UserService(repo); + } + @Test + void constructorthrowsIfNull() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new UserService(null)); + assertEquals("UserRepository cannot be null", ex.getMessage()); + } + + @Test + void testGetUserRepository() { + assertEquals(repo, service.getUserRepository()); + } + + @Test + void registerUserValid() { + boolean result = service.registerUser("Customer", "Alice", "Smith", + "alice@example.com", "$2a$10$hashed"); + assertTrue(result); + assertEquals(3, repo.getUsers().size()); + } + + @Test + void registerUserRoleCaseInsensitive() { + boolean result = service.registerUser("customer", "Bob", "Smith", + "bob@example.com", "$2a$10$hashed"); + assertTrue(result); + } + + @Test + void registerUserInvalidInputsReturnFalse() { + assertFalse(service.registerUser(null, "A", "B", "a@b.com", "pass")); + assertFalse(service.registerUser("Customer", null, "B", "a@b.com", "pass")); + assertFalse(service.registerUser("Customer", "A", null, "a@b.com", "pass")); + assertFalse(service.registerUser("Customer", "A", "B", null, "pass")); + assertFalse(service.registerUser("Customer", "A", "B", "a@b.com", null)); + + assertFalse(service.registerUser("", "A", "B", "a@b.com", "pass")); + assertFalse(service.registerUser("Customer", "", "B", "a@b.com", "pass")); + assertFalse(service.registerUser("Customer", "A", "", "a@b.com", "pass")); + assertFalse(service.registerUser("Customer", "A", "B", "", "pass")); + assertFalse(service.registerUser("Customer", "A", "B", "a@b.com", "")); + + assertFalse(service.registerUser(" ", "A", "B", "a@b.com", "pass")); + assertFalse(service.registerUser("Customer", " ", "B", "a@b.com", "pass")); + assertFalse(service.registerUser("Customer", "A", " ", "a@b.com", "pass")); + assertFalse(service.registerUser("Customer", "A", "B", " ", "pass")); + assertFalse(service.registerUser("Customer", "A", "B", "a@b.com", " ")); + } + + @Test + void registerUserDuplicateEmailAllowedInCurrentCode() { + boolean result = service.registerUser("Customer", "John", "Cena", + "john.cena@example.com", "$2a$10$hashed"); + assertTrue(result); + assertEquals(3, repo.getUsers().size()); + } + + @Test + void registerUserInvalidRoleReturnsFalse() { + boolean result = service.registerUser("Admin", "X", "Y", "x@y.com", "pass"); + assertFalse(result); + } + + @Test + void loginValidPassword() { + String plainPassword = "password123"; + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + String hashedPassword = encoder.encode(plainPassword); + User testUser = new Customer(10, "Test", "User", "test@example.com", hashedPassword); + repo.addContent(testUser); + + User result = service.login("test@example.com", plainPassword.toCharArray()); + assertNotNull(result); + assertEquals("test@example.com", result.getEmail()); + assertTrue(result.verifyPassword(plainPassword.toCharArray())); + } + + @Test + void loginReturnsCorrectUser() { + String password = "1234"; + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + + User user = new Customer( + 99, "Alice", "Smith", "alice@test.com", + encoder.encode(password) + ); + + repo.addContent(user); + + User result = service.login("alice@test.com", password.toCharArray()); + + assertNotNull(result); + assertEquals(99, result.getUserId()); + assertEquals("Alice", result.getFirstName()); + } + + @Test + void loginInvalidPassword() { + String plainPassword = "password123"; + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + String hashedPassword = encoder.encode(plainPassword); + User testUser = new Customer(10, "Test", "User", "test@example.com", hashedPassword); + repo.addContent(testUser); + + User result = service.login("test@example.com", "wrongpass".toCharArray()); + User result2 = service.login("test@example.com", null); + User result3 = service.login("test@example.com", " ".toCharArray()); + User result4 = service.login("test@example.com", new char[0]); + assertNull(result); + assertNull(result2); + assertNull(result3); + assertNull(result4); + } + + @Test + void loginInvalidEmail() { + User result = service.login("nonexist@example.com", "password".toCharArray()); + User result2 = service.login(null, "password".toCharArray()); + User result3 = service.login(" ", "password".toCharArray()); + assertNull(result); + assertNull(result2); + assertNull(result3); + } + + @Test + void getUserByEmailValid() { + User result = service.getUserByEmail("jane.doe@example.com"); + assertEquals("Jane", result.getFirstName()); + } + + @Test + void getUserByEmailNotFound() { + User result = service.getUserByEmail(""); + User result2 = service.getUserByEmail(null); + User result3 = service.getUserByEmail(" "); + assertNull(result); + assertNull(result2); + assertNull(result3); + } +} \ No newline at end of file diff --git a/src/test/java/edu/group5/app/utils/UtilitiesTest.java b/src/test/java/edu/group5/app/utils/UtilitiesTest.java new file mode 100644 index 0000000..88aa0c9 --- /dev/null +++ b/src/test/java/edu/group5/app/utils/UtilitiesTest.java @@ -0,0 +1,5 @@ +package edu.group5.app.utils; + +public class UtilitiesTest { + +} diff --git a/src/main/java/edu/group5/app/view/View.java b/src/test/java/edu/group5/app/view/ViewTest.java similarity index 59% rename from src/main/java/edu/group5/app/view/View.java rename to src/test/java/edu/group5/app/view/ViewTest.java index 2c36e4f..bd0cd83 100644 --- a/src/main/java/edu/group5/app/view/View.java +++ b/src/test/java/edu/group5/app/view/ViewTest.java @@ -1,5 +1,5 @@ package edu.group5.app.view; -public class View { +public class ViewTest { }