diff --git a/pom.xml b/pom.xml index 75290fc..5bbba22 100644 --- a/pom.xml +++ b/pom.xml @@ -55,7 +55,7 @@ org.springframework spring-core - 6.1.10 + 6.2.0 org.slf4j diff --git a/src/main/java/edu/group5/app/App.java b/src/main/java/edu/group5/app/App.java index be397d8..b9d67da 100644 --- a/src/main/java/edu/group5/app/App.java +++ b/src/main/java/edu/group5/app/App.java @@ -5,6 +5,7 @@ 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.OrganizationScraper; import edu.group5.app.model.organization.OrganizationService; import edu.group5.app.model.user.UserRepository; import edu.group5.app.model.user.UserService; @@ -62,16 +63,16 @@ public void init() { System.err.println("Failed to load organization data: " + e.getMessage()); } - // Create repositories with fetched data + // Create scraper and repositories with fetched data + OrganizationScraper orgScraper = new OrganizationScraper(); this.userRepository = new UserRepository(userData); this.donationRepository = new DonationRepository(donationData); - OrganizationRepository organizationRepository = new OrganizationRepository(organizationData); + OrganizationRepository organizationRepository = new OrganizationRepository(organizationData, orgScraper); // Create services (backend wiring) UserService userService = new UserService(this.userRepository); DonationService donationService = new DonationService(this.donationRepository, organizationRepository); - OrganizationService organizationService = new OrganizationService(organizationRepository); - + OrganizationService organizationService = new OrganizationService(organizationRepository, orgScraper); this.root = new BorderPane(); this.appState = new AppState(); this.nav = new NavigationController(root, appState, userService, donationService, organizationService); diff --git a/src/main/java/edu/group5/app/control/DonationController.java b/src/main/java/edu/group5/app/control/DonationController.java index 9118c7e..a579a9b 100644 --- a/src/main/java/edu/group5/app/control/DonationController.java +++ b/src/main/java/edu/group5/app/control/DonationController.java @@ -12,6 +12,9 @@ import java.util.Map; import java.util.Set; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; + public class DonationController { private final AppState appState; private final NavigationController nav; @@ -38,27 +41,58 @@ public Set getUniqueOrgs() { return uniqueOrgs; } - public void handleDonate() { - // Get session data from MainController + public void requestDonationConfirmation() { + // Get session data User currentUser = appState.getCurrentUser(); Organization currentOrg = appState.getCurrentOrganization(); BigDecimal amount = appState.getCurrentDonationAmount(); String paymentMethod = appState.getCurrentPaymentMethod(); + // Validate before showing dialog if (currentUser == null) { - System.err.println("Error: No user logged in"); + showError("Error: No user logged in"); return; } - if (!(currentUser instanceof Customer customer)) { - System.err.println("Error: Only customers can donate"); + if (!(currentUser instanceof Customer)) { + showError("Error: Only customers can donate"); return; } if (currentOrg == null) { - System.err.println("Error: No organization selected"); + showError("Error: No organization selected"); return; } if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) { - System.err.println("Error: Invalid donation amount"); + showError("Please select a donation amount first"); + return; + } + + // Show confirmation dialog + Alert confirmDialog = new Alert(Alert.AlertType.CONFIRMATION); + confirmDialog.setTitle("Confirm Donation"); + confirmDialog.setHeaderText("Confirm Your Donation"); + confirmDialog.setContentText( + "Organization: " + currentOrg.name() + "\n" + + "Amount: " + amount + " kr\n" + + "Payment Method: " + paymentMethod + "\n\n" + + "Are you sure you want to proceed?" + ); + + // If user clicks OK, process donation + if (confirmDialog.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { + handleDonate(); + } + // If Cancel, dialog just closes and nothing happens + } + + private void handleDonate() { + // This now only handles the actual donation processing + User currentUser = appState.getCurrentUser(); + Organization currentOrg = appState.getCurrentOrganization(); + BigDecimal amount = appState.getCurrentDonationAmount(); + String paymentMethod = appState.getCurrentPaymentMethod(); + + if (!(currentUser instanceof Customer customer)) { + System.err.println("Error: Only customers can donate"); return; } if (paymentMethod == null) { @@ -86,4 +120,12 @@ public void handleDonate() { // Navigate to payment complete nav.showPaymentCompletePage(); } + + private void showError(String message) { + Alert errorAlert = new Alert(Alert.AlertType.WARNING); + errorAlert.setTitle("Donation Error"); + errorAlert.setHeaderText("Cannot Process Donation"); + errorAlert.setContentText(message); + errorAlert.showAndWait(); + } } diff --git a/src/main/java/edu/group5/app/control/LoginController.java b/src/main/java/edu/group5/app/control/LoginController.java index 649a1c4..ae9d22e 100644 --- a/src/main/java/edu/group5/app/control/LoginController.java +++ b/src/main/java/edu/group5/app/control/LoginController.java @@ -5,6 +5,12 @@ import edu.group5.app.model.user.UserService; import edu.group5.app.view.loginpage.LoginPageView; import edu.group5.app.view.loginpage.SignUpPageView; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; + +import java.util.Arrays; + import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class LoginController { @@ -27,21 +33,38 @@ public void handleSignUp(SignUpPageView view, String firstName, String lastName, return; } - String password = new String(passwordChars); BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); - String hashedPassword = encoder.encode(password); - boolean success = userService.registerUser( - "Customer", firstName, lastName, email, hashedPassword); + // Clears password char array after creating a hash. + String hashedPassword = encoder.encode(new String(passwordChars)); + for (int i = 0; i < passwordChars.length; i++) { + passwordChars[0] = '0'; + } - if (success) { - User user = userService.getUserByEmail(email); + Alert privacyPolicy = new Alert(Alert.AlertType.CONFIRMATION); + privacyPolicy.setTitle("Accept Privacy Policy"); + privacyPolicy.setHeaderText("Accept Privacy Policy"); + privacyPolicy.setContentText( + "Your user information like:\n" + + "Name and email—as well as donations tied to your account—will be saved locally on your machine.\n" + + "By creating an account, you accept the right of our app to store this information."); - appState.setCurrentUser(user); - nav.showHomePage(); + if (privacyPolicy.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.OK) { + boolean success = userService.registerUser( + "Customer", firstName, lastName, email, hashedPassword); + + if (success) { + User user = userService.getUserByEmail(email); + + appState.setCurrentUser(user); + nav.showHomePage(); + } else { + view.showError("Registration failed. Email may already be in use."); + } } else { - view.showError("Registration failed. Email may already be in use."); + view.showError("Registration failed. Must Accept Privacy Policy to create account."); } + } public void handleLogin(LoginPageView view, String email, char[] passwordChars) { diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java index cc0a6b1..61155b7 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationRepository.java @@ -11,25 +11,33 @@ * 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 + * Delegates web scraping to OrganizationScraper for separation of concerns. */ public class OrganizationRepository extends Repository { private final HashMap grandMap; + private final OrganizationScraper scraper; /** - * 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 + * Initializes the repository with the given input data and scraper. + * Converts input into Organization objects and stores 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 + * @param scraper the OrganizationScraper to use for fetching web data, must not be null + * @throws IllegalArgumentException if input or scraper is null */ - public OrganizationRepository(Object[] input) { - super(new HashMap<>()); - grandMap = new HashMap<>(); + public OrganizationRepository(Object[] input, OrganizationScraper scraper) { + super(new HashMap<>()); + this.grandMap = new HashMap<>(); if (input == null) { throw new IllegalArgumentException("The input cannot be null"); } + if (scraper == null) { + throw new IllegalArgumentException("The scraper cannot be null"); + } + this.scraper = scraper; + ObjectMapper mapper = new ObjectMapper(); for (Object obj : input) { @@ -42,7 +50,8 @@ public OrganizationRepository(Object[] input) { 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; + String description = scraper.fetchDescription(websiteURL); + description = description != null ? description : "Information about " + name; Organization org = new Organization(orgNumber, name, trusted, websiteURL, isPreApproved, description, null); grandMap.put(org.orgNumber(), org); diff --git a/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java new file mode 100644 index 0000000..6a8c230 --- /dev/null +++ b/src/main/java/edu/group5/app/model/organization/OrganizationScraper.java @@ -0,0 +1,115 @@ +package edu.group5.app.model.organization; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.TextNode; +import org.jsoup.select.Elements; + +import java.util.stream.Collectors; + +import java.util.HashMap; +import java.util.Map; + +/** + * Handles web scraping of organization information from Innsamlingskontrollen. + * Responsible for fetching logos and descriptions from organization pages. + * All results are cached to avoid redundant network requests. + */ +public class OrganizationScraper { + private final Map logoCache = new HashMap<>(); + private final Map descriptionCache = new HashMap<>(); + + /** + * Fetches the description for the given URL by scraping all text content + * inside {@code
}. Results are cached. + * + *

Strategy:

+ *
    + *
  1. Tries to get all <p> tags (skipping the first one) and concatenates them
  2. + *
  3. If no paragraphs found, gets all text content from the section
  4. + *
  5. Returns null if section not found or is empty
  6. + *
+ * + * @param pageUrl the URL for the organization's page; may be null or blank + * @return the description text, or null if not found or pageUrl is invalid + */ + public String fetchDescription(String pageUrl) { + if (pageUrl == null || pageUrl.isBlank()) { + return null; + } + + if (descriptionCache.containsKey(pageUrl)) { + return descriptionCache.get(pageUrl); + } + + try { + Document doc = Jsoup.connect(pageUrl) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .timeout(5000).get(); + + Element section = doc.selectFirst("section.information"); + if (section != null) { + section.select("div.extra-info").remove(); + section.select("a.read-more").remove(); + + // Extract all

tags and

elements as separate paragraphs + String description = section.select("p, div").stream() + .filter(el -> el.tagName().equals("p") || el.select("p").isEmpty()) + .filter(el -> !el.hasClass("extra-info") && !el.hasClass("logo")) + .map(Element::text) + .map(text -> text.replace("Les mer", "").trim()) + .filter(text -> !text.isBlank()) + .collect(Collectors.joining("\n\n")); + + // Fallback: if no paragraphs found, get all text from section + if (description.isBlank()) { + description = section.text().trim(); + } + description = description.replace("Les mer", "").trim(); + + // Only cache and return if we found something meaningful + if (!description.isBlank()) { + descriptionCache.put(pageUrl, description); + return description; + } + } + } catch (Exception e) { + System.out.println("Could not get description for: " + pageUrl); + } + return null; + } + + /** + * Fetches the logo URL for the given page by scraping the {@code div.logo img} + * element. Results are cached so each URL is only fetched once. + * + * @param pageUrl the URL for the organization's page; may be null or blank + * @return the absolute logo URL, or null if not found or pageUrl is invalid + */ + public String fetchLogoUrl(String pageUrl) { + if (pageUrl == null || pageUrl.isBlank()) { + return null; + } + + if (logoCache.containsKey(pageUrl)) { + return logoCache.get(pageUrl); + } + + try { + Document doc = Jsoup.connect(pageUrl) + .userAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .timeout(5000).get(); + Element img = doc.selectFirst("div.logo img"); + + if (img != null) { + String logoUrl = img.absUrl("src"); + logoCache.put(pageUrl, logoUrl); + return logoUrl; + } + } catch (Exception e) { + System.out.println("Could not get logo for: " + pageUrl); + } + return 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 index 9785040..e19c273 100644 --- a/src/main/java/edu/group5/app/model/organization/OrganizationService.java +++ b/src/main/java/edu/group5/app/model/organization/OrganizationService.java @@ -1,10 +1,9 @@ package edu.group5.app.model.organization; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; +import java.util.stream.Collectors; -import java.util.HashMap; +import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -13,26 +12,29 @@ * It interacts with the OrganizationRepository to retrieve organization information * and contains business logic associated with organization management. * - *

It provides fetching logo URLs by web scraping each organization's page on - * Innsamlingskontrollen.

+ *

It provides fetching logo URLs by delegating to OrganizationScraper.

* * Fetched logo URLs are cached to avoid redundant network requests. */ public class OrganizationService { private OrganizationRepository organizationRepository; - - private final Map logoCache = new HashMap<>(); + private OrganizationScraper scraper; /** - * Constructs an OrganizationService with the given OrganizationRepository. + * Constructs an OrganizationService with the given OrganizationRepository and scraper. * @param organizationRepository the OrganizationRepository to use for managing organization data; must not be null - * @throws IllegalArgumentException if organizationRepository is null + * @param scraper the OrganizationScraper to use for fetching web data; must not be null + * @throws IllegalArgumentException if organizationRepository or scraper is null */ - public OrganizationService(OrganizationRepository organizationRepository) { + public OrganizationService(OrganizationRepository organizationRepository, OrganizationScraper scraper) { if (organizationRepository == null) { throw new IllegalArgumentException("OrganizationRepository cannot be null"); } + if (scraper == null) { + throw new IllegalArgumentException("OrganizationScraper cannot be null"); + } this.organizationRepository = organizationRepository; + this.scraper = scraper; } /** @@ -48,7 +50,10 @@ public OrganizationRepository getOrganizationRepository() { * @return a map of trusted organizations by organization number */ public Map getTrustedOrganizations() { - return organizationRepository.getTrustedOrganizations(); + return organizationRepository.getTrustedOrganizations().values().stream() + .sorted(Comparator.comparing(Organization::name)) + .collect(Collectors.toMap(Organization::orgNumber, + org -> org, (e1, e2) -> e1, LinkedHashMap::new)); } /** @@ -80,59 +85,30 @@ public Organization findByOrgName(String name) { * @return the absolute logo URL, or null if not found or pageUrl is invalid */ - public String fetchLogoUrl(String pageUrl) { - if (pageUrl == null || pageUrl.isBlank()) { - return null; - } - - if (logoCache.containsKey(pageUrl)) { - return logoCache.get(pageUrl); - } - - try { - Document doc = Jsoup.connect(pageUrl).get(); - Element img = doc.selectFirst("div.logo img"); - - if (img != null) { - String logoUrl = img.absUrl("src"); - logoCache.put(pageUrl, logoUrl); - return logoUrl; - } - } catch (Exception e) { - System.out.println("Could not get logo for: " + pageUrl); - } - return null; - } - /** - * Fetches all trusted organizations with their logo URLs. + * Fetches all trusted organizations with their logo URLs and descriptions. * *

- * For each trusted organization, attempts to get its logo using - * {@link #fetchLogoUrl(String)}. Creates a new Organization - * object including the logo URL. + * For each trusted organization, attempts to get its logo using the scraper. + * Creates a new Organization object including the logo URL (description is + * already fetched during repository initialization). *

* @return a map of trusted organizations keyed by organization number, with logos included */ public Map getTrustedOrganizationsWithLogos() { Map original = getTrustedOrganizations(); - Map trustedOrgsWithLogos = new HashMap<>(); - - for (Organization org : original.values()) { - String logoUrl = fetchLogoUrl(org.websiteUrl()); - - Organization newOrg = new Organization( - org.orgNumber(), - org.name(), - org.trusted(), - org.websiteUrl(), - org.isPreApproved(), - org.description(), - logoUrl - ); - trustedOrgsWithLogos.put(newOrg.orgNumber(), newOrg); - } - return trustedOrgsWithLogos; + return original.values().parallelStream() + .map(org -> new Organization( + org.orgNumber(), + org.name(), + org.trusted(), + org.websiteUrl(), + org.isPreApproved(), + org.description(), + scraper.fetchLogoUrl(org.websiteUrl()) + )) + .sorted(Comparator.comparing(Organization::name)) + .collect(Collectors.toMap(Organization::orgNumber, org -> org, (e1, e2) -> e1, LinkedHashMap::new)); } /** diff --git a/src/main/java/edu/group5/app/model/user/UserService.java b/src/main/java/edu/group5/app/model/user/UserService.java index 628c785..b237eae 100644 --- a/src/main/java/edu/group5/app/model/user/UserService.java +++ b/src/main/java/edu/group5/app/model/user/UserService.java @@ -1,16 +1,21 @@ 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. + * 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 + * + * @param userRepository the UserRepository to use for managing user data; must + * not be null * @throws IllegalArgumentException if userRepository is null */ public UserService(UserRepository userRepository) { @@ -22,7 +27,9 @@ public UserService(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. + * 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() { @@ -30,31 +37,39 @@ public UserRepository getUserRepository() { } /** - * 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 + * 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) { + 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()) { + passwordHash == null || passwordHash.trim().isEmpty() || + this.getUserByEmail(email) != null) { 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*/ + } else { return false; } this.userRepository.addContent(user); @@ -63,15 +78,19 @@ public boolean registerUser(String role, String firstName, String lastName, /** * 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 + * + * @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 + * (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; + return null; } User user = this.userRepository.findUserByEmail(email); if (user != null && user.verifyPassword(password)) { @@ -82,6 +101,7 @@ public User login(String email, char[] password) { /** * 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 */ diff --git a/src/main/java/edu/group5/app/utils/ParameterValidator.java b/src/main/java/edu/group5/app/utils/ParameterValidator.java new file mode 100644 index 0000000..1a44e46 --- /dev/null +++ b/src/main/java/edu/group5/app/utils/ParameterValidator.java @@ -0,0 +1,71 @@ +package edu.group5.app.utils; + +import java.math.BigDecimal; +/** + * ParameterValidator is a utility class that provides static methods for validating various types of parameters. + * It includes methods for checking strings, integers, objects, and BigDecimal values to ensure they meet specific + * criteria such as not being null, not being blank, or being positive. + * + */ +public final class ParameterValidator { + + /** + * Validates that a string parameter is not null and not blank. + * @param stringArg the string parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the string is null or blank + */ + public static final void stringChecker(String stringArg, String variableName) throws IllegalArgumentException { + nullCheck(stringArg, variableName); + if (stringArg.isBlank()) { + throw new IllegalArgumentException(String.format("%s can't be blank", variableName)); + } + } + + /** + * Validates that an integer parameter is not null and is a positive integer. + * @param intArg the integer parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the integer is null or not a positive integer + */ + public static final void intChecker(int intArg, String variableName) throws IllegalArgumentException { + if (intArg <= 0) { + throw new IllegalArgumentException(String.format("%s must be a positive integer", variableName)); + } + } + + /** + * Validates that an object parameter is not null. + * @param objectArg the object parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the object is null + */ + public static final void objectChecker(Object objectArg, String variableName) throws IllegalArgumentException { + nullCheck(objectArg, variableName); + } + + /** + * Validates that a BigDecimal parameter is not null and is greater than zero. + * @param bigDecimalArg the BigDecimal parameter to validate + * @param variableName the name of the variable being validated, used in exception messages + * @throws IllegalArgumentException if the BigDecimal is null or not greater than zero + */ + public static final void bigDecimalChecker(BigDecimal bigDecimalArg, String variableName) throws IllegalArgumentException { + nullCheck(bigDecimalArg, variableName); + if (bigDecimalArg.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException(String.format("%s must be larger than 0", variableName)); + } + } + + /** + * Helper method to check if a variable is null and throw an IllegalArgumentException with a formatted message if it is. + * @param variable the variable to check for null + * @param variableName the name of the variable being checked, used in the exception message + * @throws IllegalArgumentException if the variable is null + */ + private static final void nullCheck(Object variable, String variableName) throws IllegalArgumentException { + if (variable == null) { + throw new IllegalArgumentException(String.format("%s can't be null", variableName)); + } + } +} diff --git a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java index fbf438a..f6eb2b5 100644 --- a/src/main/java/edu/group5/app/view/causespage/CausesPageView.java +++ b/src/main/java/edu/group5/app/view/causespage/CausesPageView.java @@ -10,8 +10,10 @@ import javafx.scene.layout.*; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.stream.Collectors; +import java.util.Comparator; /** * A view for the causes page. @@ -47,7 +49,10 @@ public CausesPageView(AppState appState, NavigationController nav, OrganizationC setCenter(createBody()); } - private ScrollPane createBody() { + private BorderPane createBody() { + BorderPane bodyRoot = new BorderPane(); + bodyRoot.setTop(createSearchSection()); + ScrollPane body = new ScrollPane(); body.setId("body"); body.setFitToWidth(true); @@ -58,12 +63,40 @@ private ScrollPane createBody() { vBox.setStyle("-fx-padding: 10;"); vBox.setSpacing(10); vBox.setMaxWidth(Double.MAX_VALUE); - vBox.getChildren().addAll( - createSearchSection(), - createOrganizationSection(null) - ); + + // Load organizations INSTANTLY from cache + allOrganizations = orgController.getTrustedOrgs(); + + vBox.getChildren().add(createOrganizationSection(null)); body.setContent(vBox); - return body; + bodyRoot.setCenter(body); + + // Build a map of org ID -> card for quick lookup + Map cardMap = new HashMap<>(); + for (var node : organizationGrid.getChildren()) { + if (node instanceof OrganizationCard card) { + cardMap.put(card.getOrganization().orgNumber(), card); + } + } + + // Fetch logos and update existing cards (don't rebuild grid) + orgController.getOrganizationsWithLogosAsync() + .thenAccept(orgs -> {this.allOrganizations = orgs; + Platform.runLater(() -> { + for (var entry : orgs.entrySet()) { + OrganizationCard card = cardMap.get(entry.getKey()); + if (card != null && entry.getValue().logoUrl() != null) { + card.updateLogo(entry.getValue().logoUrl()); + } + } + Organization currentOrg = appState.getCurrentOrganization(); + if (currentOrg != null && orgs.containsKey(currentOrg.orgNumber())) { + appState.setCurrentOrganization(orgs.get(currentOrg.orgNumber())); + } + }); + }); + + return bodyRoot; } private HBox createSearchSection() { @@ -93,25 +126,8 @@ private GridPane createOrganizationSection(String searchTerm) { organizationGrid = grid; } - if (allOrganizations == null) { - allOrganizations = orgController.getTrustedOrgs(); - - //Show loading text while organizations and logos are fetched - grid.add(new javafx.scene.control.Label("Loading..."), 0, 0); - - //Fetch trusted organizations with logos asynchronously (runs in background) - orgController.getOrganizationsWithLogosAsync() - .thenAccept(orgs -> { - this.allOrganizations = orgs; - - // Update UI when data is ready - Platform.runLater(() -> updateOrganizationGrid("")); - }); - return grid; - } - Map organizations = new HashMap<>(); - if (searchTerm != null) { + if (searchTerm != null && !searchTerm.isEmpty()) { // Filter organizations by search term organizations = filterOrganizations(searchTerm); } else { @@ -162,9 +178,12 @@ private Map filterOrganizations(String searchTerm) { String lowerSearchTerm = searchTerm.toLowerCase(); return allOrganizations.values().stream() .filter(org -> org.name().toLowerCase().contains(lowerSearchTerm)) + .sorted(Comparator.comparing(Organization::name)) .collect(Collectors.toMap( Organization::orgNumber, - org -> org + org -> org, + (e1, e2) -> e1, + LinkedHashMap::new )); } diff --git a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java index e42ebd5..2ca30bb 100644 --- a/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java +++ b/src/main/java/edu/group5/app/view/causespage/OrganizationCard.java @@ -24,6 +24,8 @@ public class OrganizationCard extends VBox { private final AppState appState; private final Organization organization; private final NavigationController nav; + private StackPane imageContainer; + private String currentLogoUrl; public OrganizationCard(AppState appstate, NavigationController nav, Organization org, String img) { this.appState = appstate; @@ -32,14 +34,15 @@ public OrganizationCard(AppState appstate, NavigationController nav, Organizatio setId("mainContainer"); getStylesheets().add(getClass().getResource("/browsepage/browse_org.css").toExternalForm()); + imageContainer = createImageContainer(img); getChildren().addAll( - imageContainer(img), + imageContainer, orgName(org.name()), checkMarkContainer() ); setOnMouseClicked(e -> { - appstate.setCurrentOrganization(organization); + appstate.setCurrentOrganization(getOrganizationWithCurrentLogo()); nav.showOrganizationPage(); }); @@ -48,7 +51,41 @@ public OrganizationCard(AppState appstate, NavigationController nav, Organizatio setAlignment(Pos.CENTER); } - private StackPane imageContainer(String img) { + public Organization getOrganization() { + return organization; + } + + public void updateLogo(String logoUrl) { + this.currentLogoUrl = logoUrl; + if (imageContainer == null) return; + imageContainer.getChildren().clear(); + if (logoUrl != null && !logoUrl.isBlank()) { + ImageView logo = new ImageView(new Image(logoUrl, true)); + logo.setId("logo"); + logo.setSmooth(true); + logo.setPreserveRatio(true); + logo.setFitHeight(80); + logo.setFitWidth(80); + imageContainer.getChildren().add(logo); + } + } + + private Organization getOrganizationWithCurrentLogo() { + if (currentLogoUrl == null) { + return organization; + } + return new Organization( + organization.orgNumber(), + organization.name(), + organization.trusted(), + organization.websiteUrl(), + organization.isPreApproved(), + organization.description(), + currentLogoUrl + ); + } + + private StackPane createImageContainer(String img) { StackPane imageContainer = new StackPane(); imageContainer.setId("imageContainer"); diff --git a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java index f9681fc..9444421 100644 --- a/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java +++ b/src/main/java/edu/group5/app/view/donationpage/DonationPageView.java @@ -5,8 +5,8 @@ import edu.group5.app.model.AppState; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.control.Button; import javafx.scene.control.TextField; +import javafx.scene.control.Button; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.TilePane; @@ -16,7 +16,7 @@ import javafx.scene.Node; import java.math.BigDecimal; -import java.util.*; +import java.util.Objects; /** * A view for the Donation Page. @@ -31,13 +31,9 @@ public class DonationPageView extends BorderPane { private final NavigationController nav; private final DonationController donationController; - private final List allDonationElements = new ArrayList<>(); - private final List allPaymentElements = new ArrayList<>(); - private final Map elementAmounts = new HashMap<>(); - private final Map elementPaymentMethods = new HashMap<>(); - - private BigDecimal selectedAmount = null; - private String selectedPaymentMethod = null; + private Node currentlySelected = null; + private TextField customAmountField; + private Node selectedPaymentMethod = null; private Button donateBtn; public DonationPageView(AppState appState, NavigationController nav, DonationController donationController) { @@ -49,9 +45,16 @@ public DonationPageView(AppState appState, NavigationController nav, DonationCon VBox content = new VBox(); content.getChildren().addAll(createBackButton(), createDonationGrid(), createPaymentMethodSection(), createDonateSection()); + + content.setOnMouseClicked(e -> { + if (e.getTarget() == content) { + clearSelection(); + } + }); setCenter(content); } + private HBox createBackButton() { Button backBtn = new Button("←"); backBtn.getStyleClass().add("back-button"); @@ -61,6 +64,7 @@ private HBox createBackButton() { container.setPadding(new Insets(10, 0, 0, 10)); return container; } + private TilePane createDonationGrid(){ TilePane body = new TilePane(); body.setAlignment(Pos.CENTER); @@ -95,14 +99,12 @@ public Button createDonationButton(String title, String amount) { button.getStyleClass().add("donation-button"); BigDecimal parsedAmount = parseAmount(amount); - elementAmounts.put(button, parsedAmount); + button.setUserData(parsedAmount); - button.setOnAction(e -> { - selectDonationElement(button); - }); - allDonationElements.add(button); + button.setOnAction(e -> selectDonation(button)); return button; } + private VBox createCustomButton() { Text titleText = new Text("Custom Donation"); titleText.getStyleClass().add("donation-title"); @@ -113,8 +115,10 @@ private VBox createCustomButton() { Text krText = new Text("kr"); krText.getStyleClass().add("donation-amount"); - TextField amountField = new TextField(); + this.customAmountField = new TextField(); + TextField amountField = customAmountField; amountField.getStyleClass().add("donation-input"); + amountField.setPromptText("Enter amount"); amountRow.getChildren().addAll(amountField, krText); @@ -123,40 +127,66 @@ private VBox createCustomButton() { box.getStyleClass().add("donation-button"); box.setOnMouseClicked(e -> { - try { - String text = amountField.getText().trim(); - if (text.isEmpty()) { - return; - } - BigDecimal amount = new BigDecimal(text); + selectDonation(box); + amountField.requestFocus(); + }); - elementAmounts.put(box, amount); - selectDonationElement(box); - } catch (NumberFormatException exception) { - System.err.println("Invalid custom donation amount: " + amountField.getText()); - } + amountField.setOnMouseClicked(e -> + { + selectDonation(box); + amountField.requestFocus(); }); - allDonationElements.add(box); + // NEW: On text field input - update the amount in real-time + amountField.textProperty().addListener((obs, oldVal, newVal) -> { + if (!newVal.trim().isEmpty()) { + try { + BigDecimal amount = new BigDecimal(newVal.trim()); + box.setUserData(amount); + updateDonationAmount(amount); + } catch (NumberFormatException ignored) { + // User is still typing, silently ignore + } + } else { + box.setUserData(null); + if (currentlySelected == box) { + updateDonationAmount(null); + } + } + }); return box; } + private HBox createDonateSection() { + donateBtn = new Button("Donate"); + donateBtn.getStyleClass().add("donate-button"); + donateBtn.setDisable(true); + donateBtn.setOnAction(e -> donationController.requestDonationConfirmation()); + + Button clearBtn = new Button("Clear"); + clearBtn.getStyleClass().add("clear-button"); + clearBtn.setOnAction(e -> clearSelection()); + + HBox section = new HBox(20, clearBtn, donateBtn); + section.setAlignment(Pos.CENTER); + section.setPadding(new Insets(20, 0, 30, 0)); + return section; + } + public HBox createPaymentMethodSection() { Button appleBtn = new Button("Apple Pay"); Button vippsBtn = new Button("Vipps"); Button visaBtn = new Button("Visa"); + appleBtn.setUserData("Apple Pay"); + vippsBtn.setUserData("Vipps"); + visaBtn.setUserData("Visa"); + for (Button btn : new Button[]{appleBtn, vippsBtn, visaBtn}) { btn.getStyleClass().add("payment-method-button"); - - btn.setOnAction(e -> {selectPaymentMethod(btn);}); - allPaymentElements.add(btn); + btn.setOnAction(e -> selectPaymentMethod(btn)); } - elementPaymentMethods.put(appleBtn, "Apple Pay"); - elementPaymentMethods.put(vippsBtn, "Vipps"); - elementPaymentMethods.put(visaBtn, "Visa"); - HBox sectionPm = new HBox(appleBtn, vippsBtn, visaBtn); sectionPm.setAlignment(Pos.CENTER); sectionPm.setSpacing(20); @@ -164,55 +194,51 @@ public HBox createPaymentMethodSection() { return sectionPm; } - private HBox createDonateSection() { - donateBtn = new Button("Donate"); - donateBtn.getStyleClass().add("donate-button"); - - donateBtn.setDisable(true); - - donateBtn.setOnAction(e -> donationController.handleDonate()); + private void selectDonation(Node element) { + if (currentlySelected != null) { + currentlySelected.getStyleClass().remove("donation-button-selected"); + } + currentlySelected = element; + currentlySelected.getStyleClass().add("donation-button-selected"); - HBox section = new HBox(donateBtn); - section.setAlignment(Pos.CENTER); - return section; + BigDecimal amount = (BigDecimal) element.getUserData(); + updateDonationAmount(amount); + updateDonationButtonState(); } - private void selectDonationElement(Node element) { - // Remove selected class from all elements - for (Node node : allDonationElements) { - node.getStyleClass().remove("donation-button-selected"); + private void selectPaymentMethod(Node element) { + if (selectedPaymentMethod != null) { + selectedPaymentMethod.getStyleClass().remove("payment-method-selected"); } + selectedPaymentMethod = element; + selectedPaymentMethod.getStyleClass().add("payment-method-selected"); - element.getStyleClass().add("donation-button-selected"); - - // Extract and store the amount - extractAndStoreAmount(element); + String paymentMethod = (String) element.getUserData(); + appState.setCurrentPaymentMethod(paymentMethod); + updateDonationButtonState(); } - private void selectPaymentMethod(Node element) { - for (Node node : allPaymentElements) { - node.getStyleClass().remove("payment-method-selected"); + + private void clearSelection() { + if (currentlySelected != null) { + currentlySelected.getStyleClass().remove("donation-button-selected"); + currentlySelected = null; + updateDonationAmount(null); } - element.getStyleClass().add("payment-method-selected"); - extractAndStorePaymentMethod(element); - } + if (selectedPaymentMethod != null) { + selectedPaymentMethod.getStyleClass().remove("payment-method-selected"); + selectedPaymentMethod = null; + } - private void extractAndStoreAmount(Node element) { - BigDecimal amount = elementAmounts.get(element); - if (amount != null) { - selectedAmount = amount; - appState.setCurrentDonationAmount(amount); - updateDonationButtonState(); + if (customAmountField != null) { + customAmountField.clear(); } + + updateDonationButtonState(); } - private void extractAndStorePaymentMethod(Node element) { - String paymentMethod = elementPaymentMethods.get(element); - if (paymentMethod != null) { - selectedPaymentMethod = paymentMethod; - appState.setCurrentPaymentMethod(paymentMethod); - updateDonationButtonState(); - } + private void updateDonationAmount(BigDecimal amount) { + appState.setCurrentDonationAmount(amount); } private BigDecimal parseAmount(String amountStr) { @@ -222,8 +248,9 @@ private BigDecimal parseAmount(String amountStr) { return BigDecimal.ZERO; } } + private void updateDonationButtonState() { - donateBtn.setDisable(selectedAmount == null || selectedPaymentMethod == null); + donateBtn.setDisable(currentlySelected == null || selectedPaymentMethod == null); } } \ No newline at end of file diff --git a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java index 77ea7ab..53c623b 100644 --- a/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java +++ b/src/main/java/edu/group5/app/view/organizationpage/OrganizationPageView.java @@ -9,6 +9,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextArea; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; @@ -82,7 +83,7 @@ private StackPane createImageContainer() { imageContainer.setId("imageContainer"); imageContainer.setPrefHeight(120); imageContainer.setPrefWidth(120); - imageContainer.setMaxWidth(Double.MAX_VALUE); + imageContainer.setMaxWidth(120); Organization org = appState.getCurrentOrganization(); if (org != null && org.logoUrl() != null && !org.logoUrl().isBlank()) { @@ -90,6 +91,8 @@ private StackPane createImageContainer() { logo.setId("logo"); logo.setSmooth(true); logo.setPreserveRatio(true); + logo.setFitHeight(350); + logo.setFitWidth(350); imageContainer.getChildren().add(logo); } else { StackPane placeholder = new StackPane(); @@ -110,14 +113,40 @@ private VBox createOrgInfoSection() { orgInfoSection.setSpacing(50); VBox orgNameAndDescription = new VBox(); + orgNameAndDescription.setSpacing(5); 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"); + VBox descriptionBox = new VBox(); + descriptionBox.setSpacing(15); + descriptionBox.setId("description-container"); + descriptionBox.setMaxWidth(750); + + if (org != null && org.description() != null) { + String[] rawParagraphs = org.description().split("\n{2,}"); + + for (String para : rawParagraphs) { + String cleaned = para.trim(); + if (!cleaned.isBlank()) { + Label paragraph = new Label(cleaned); + paragraph.setId("description-paragraph"); + paragraph.setWrapText(true); + descriptionBox.getChildren().add(paragraph); + } + } + } + + ScrollPane descriptionScroll = new ScrollPane(descriptionBox); + descriptionScroll.setId("description-scroll"); + descriptionScroll.setFitToWidth(true); + descriptionScroll.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); + descriptionScroll.setPrefHeight(400); + descriptionScroll.setMaxHeight(400); + descriptionScroll.setPrefWidth(750); + descriptionScroll.setMinWidth(750); - orgNameAndDescription.getChildren().addAll(orgName, description); + orgNameAndDescription.getChildren().addAll(orgName, descriptionScroll); Button donateBtn = new Button("Donate"); donateBtn.setId("donate-button"); diff --git a/src/main/java/edu/group5/app/view/userpage/UserPageView.java b/src/main/java/edu/group5/app/view/userpage/UserPageView.java index b410761..e3b7c8b 100644 --- a/src/main/java/edu/group5/app/view/userpage/UserPageView.java +++ b/src/main/java/edu/group5/app/view/userpage/UserPageView.java @@ -13,12 +13,16 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.Text; +import java.text.SimpleDateFormat; import java.math.RoundingMode; import java.util.*; @@ -79,30 +83,30 @@ 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)); - - User currentUser = appState.getCurrentUser(); + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + scrollPane.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); + + FlowPane causesFlow = new FlowPane(10, 10); + causesFlow.setStyle("-fx-padding: 10;"); + causesFlow.getStyleClass().add("section-box"); Set uniqueOrgs = donationController.getUniqueOrgs(); if (uniqueOrgs.isEmpty()) { Label noCauses = new Label("No causes supported yet"); noCauses.setStyle("-fx-text-fill: #999;"); - causesBox.getChildren().add(noCauses); + causesFlow.getChildren().add(noCauses); } else { for (int orgId : uniqueOrgs) { Organization org = organizationController.getOrgById(orgId); if (org != null) { - Label causeLabel = new Label("• " + org.name()); - causesBox.getChildren().add(causeLabel); + causesFlow.getChildren().add(createCauseChip(org)); } } } - ScrollPane scrollPane = new ScrollPane(causesBox); - scrollPane.setFitToWidth(true); - scrollPane.setPrefHeight(150); + scrollPane.setPrefHeight(275); + scrollPane.setContent(causesFlow); return new VBox(10, title, scrollPane); } @@ -111,13 +115,59 @@ private VBox createDonationsSection() { Text title = new Text("PREVIOUS DONATIONS"); title.getStyleClass().add("section-title"); - VBox donationsBox = new VBox(10); - donationsBox.getStyleClass().add("section-box"); + HBox searchBox = new HBox(10); + searchBox.setStyle("-fx-padding: 10;"); + TextField searchField = new TextField(); + searchField.setPromptText("Search by organization name..."); + searchField.setPrefWidth(300); + searchBox.getChildren().add(searchField); + + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(false); + scrollPane.setPrefWidth(650); + scrollPane.setMaxWidth(650); + scrollPane.setPrefHeight(400); + scrollPane.setStyle("-fx-focus-color: transparent; -fx-faint-focus-color: transparent;"); + + VBox donationsBox = new VBox(12); + donationsBox.getStyleClass().add("donation-list"); donationsBox.setPadding(new Insets(10)); User currentUser = appState.getCurrentUser(); - - Map userDonations = donationController.getUserDonations(currentUser.getUserId()); + Map userDonations = + donationController.getUserDonations(currentUser.getUserId()); + + // Filter donations based on search + searchField.textProperty().addListener((obs, oldVal, newVal) -> { + donationsBox.getChildren().clear(); + + if (userDonations.isEmpty()) { + Label noDonations = new Label("No donations yet"); + noDonations.setStyle("-fx-text-fill: #999;"); + donationsBox.getChildren().add(noDonations); + return; + } + + String searchTerm = newVal.toLowerCase().trim(); + boolean found = false; + + for (Donation donation : userDonations.values()) { + Organization org = organizationController.getOrgById(donation.organizationId()); + String orgName = (org != null) ? org.name() : "Unknown Organization"; + + // Filter by search term + if (searchTerm.isEmpty() || orgName.toLowerCase().contains(searchTerm)) { + donationsBox.getChildren().add(createDonationCard(donation)); + found = true; + } + } + + if (!found && !searchTerm.isEmpty()) { + Label noResults = new Label("No donations found for \"" + newVal + "\""); + noResults.setStyle("-fx-text-fill: #999;"); + donationsBox.getChildren().add(noResults); + } + }); if (userDonations.isEmpty()) { Label noDonations = new Label("No donations yet"); @@ -125,21 +175,50 @@ private VBox createDonationsSection() { donationsBox.getChildren().add(noDonations); } else { for (Donation donation : userDonations.values()) { - Organization org = organizationController.getOrgById(donation.organizationId()); - String orgName = (org != null) ? org.name() : "Unknown Organization"; + donationsBox.getChildren().add(createDonationCard(donation)); - Label donationLabel = new Label( - orgName + " • " + donation.amount().setScale(2, RoundingMode.HALF_UP) + " kr" + " • " + - donation.date() + " • " + donation.paymentMethod() - ); - donationsBox.getChildren().add(donationLabel); } } - ScrollPane scrollPane = new ScrollPane(donationsBox); - scrollPane.setFitToWidth(true); - scrollPane.setPrefHeight(200); + scrollPane.setContent(donationsBox); + return new VBox(10, title, searchBox, scrollPane); - return new VBox(10, title, scrollPane); } + + private Label createCauseChip(Organization org) { + Label chip = new Label(org.name()); + chip.getStyleClass().add("cause-chip"); + return chip; + } + + private BorderPane createDonationCard(Donation donation) { + Organization org = organizationController.getOrgById(donation.organizationId()); + String orgName = (org != null) ? org.name() : "Unknown Organization"; + + // Use BorderPane to fix columns: LEFT | SPACE | RIGHT + BorderPane card = new BorderPane(); + card.getStyleClass().add("donation-card"); + card.setPadding(new Insets(12, 15, 12, 15)); + + // LEFT: Organization name + Text orgText = new Text(orgName); + orgText.getStyleClass().add("donation-org-name"); + card.setLeft(orgText); + + // RIGHT: Amount and date (stacked vertically) + VBox details = new VBox(4); + details.setAlignment(Pos.CENTER_RIGHT); + + Label amountLabel = new Label(String.format("%.2f", donation.amount()) + " kr"); + amountLabel.getStyleClass().add("donation-amount"); + + Label dateLabel = new Label( + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(donation.date())); + dateLabel.getStyleClass().add("donation-date"); + + details.getChildren().addAll(amountLabel, dateLabel); + card.setRight(details); + + return card; + } } diff --git a/src/main/resources/donationpage/donation.css b/src/main/resources/donationpage/donation.css index 74851a4..bc777b6 100644 --- a/src/main/resources/donationpage/donation.css +++ b/src/main/resources/donationpage/donation.css @@ -1,23 +1,24 @@ .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; + -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; + -fx-border-color: #f0f0f0; } .donation-button-selected { - -fx-background-color: #111; - -fx-text-fill: white; - -fx-border-color: #111; + -fx-background-color: #111; + -fx-text-fill: white; + -fx-border-color: #111; } + .donation-button-selected:hover { -fx-background-color: #222; -fx-border-color: #222; @@ -32,34 +33,82 @@ } .donation-title { - -fx-font-size: 18px; - -fx-font-weight: bold; - -fx-fill: #111; + -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; + -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; + -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; + -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; + -fx-background-color: #c02020; +} + +.clear-button { + -fx-pref-height: 55px; + -fx-background-color: #f0f0f0; + -fx-text-fill: #333; + -fx-font-size: 16px; + -fx-font-weight: bold; + -fx-background-radius: 8; + -fx-cursor: hand; + -fx-padding: 0 30 0 30; + -fx-border-color: #ccc; + -fx-border-width: 1; +} + +.clear-button:hover { + -fx-background-color: #e0e0e0; +} + +.clear-button:pressed { + -fx-background-color: #d0d0d0; +} + +.donation-button-selected .donation-title { + -fx-fill: white; } +.donation-button-selected .donation-amount { + -fx-fill: white; +} + +.donation-button-selected .donation-input { + -fx-text-fill: white; +} + + +.donation-button-selected .donation-input:focused { + -fx-text-fill: white; + -fx-border-color: transparent transparent white transparent; +} .payment-method-button { -fx-background-color: #111; -fx-text-fill: white; diff --git a/src/main/resources/loginpage/login.css b/src/main/resources/loginpage/login.css index dc758a1..f0ffc41 100644 --- a/src/main/resources/loginpage/login.css +++ b/src/main/resources/loginpage/login.css @@ -1,25 +1,35 @@ #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%; + -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; + -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; + -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; + -fx-border-color: #ccc; + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-padding: 24px; + -fx-max-width: 340px; +} + +#login-btn:hover { + -fx-cursor: hand; +} + +#register-btn:hover { + -fx-cursor: hand; } \ No newline at end of file diff --git a/src/main/resources/loginpage/signup.css b/src/main/resources/loginpage/signup.css index 93ad133..fcd8751 100644 --- a/src/main/resources/loginpage/signup.css +++ b/src/main/resources/loginpage/signup.css @@ -1,25 +1,35 @@ #image-section { - -fx-background-image: url("/loginpage/signup-image.png"); - -fx-background-size: auto; - -fx-background-position: right center; - -fx-background-repeat: no-repeat; - -fx-pref-width: 50%; + -fx-background-image: url("/loginpage/signup-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; + -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; + -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; + -fx-border-color: #ccc; + -fx-border-radius: 8px; + -fx-border-width: 1px; + -fx-padding: 24px; + -fx-max-width: 340px; +} + +#login-btn:hover { + -fx-cursor: hand; +} + +#register-btn:hover { + -fx-cursor: hand; } \ No newline at end of file diff --git a/src/main/resources/organizationpage/organizationpage.css b/src/main/resources/organizationpage/organizationpage.css index 8459526..20a9ad8 100644 --- a/src/main/resources/organizationpage/organizationpage.css +++ b/src/main/resources/organizationpage/organizationpage.css @@ -1,33 +1,67 @@ #main-container { - -fx-padding: 50px + -fx-padding: 50px } #logo { - -fx-min-height: 50%; + -fx-min-height: 80%; } #orgName { - -fx-font-weight: bold; - -fx-font-size: 20pt; + -fx-font-weight: bold; + -fx-font-size: 28pt; + -fx-padding: 0 0 30 0; +} + +#description-container { + -fx-padding: 30; + -fx-spacing: 22; + -fx-max-width: 750; + -fx-background-color: #f8f9fa; + -fx-border-radius: 8; } -#description { - -fx-font-size: 10pt; +#description-paragraph { + -fx-font-size: 18; + -fx-text-fill: #333; + -fx-font-family: "Segoe UI", Arial, sans-serif; + -fx-line-spacing: 13; + -fx-text-alignment: Left; + -fx-wrap-text: true; + -fx-padding: 10 0 10 0; } #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; + -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; + -fx-margin-top: 30; } #donate-button:hover { - -fx-background-color: #c02020; + -fx-background-color: #c02020; +} + +#description-scroll { + -fx-control-inner-background: #f8f9fa; + -fx-padding: 0; +} + +#description-scroll .scroll-bar:vertical { + -fx-pref-width: 8; +} + +#description-scroll .scroll-bar:vertical .thumb { + -fx-background-radius: 4; + -fx-background-color: #cccccc; +} + +#description-scroll .scroll-bar:vertical .thumb:hover { + -fx-background-color: #999999; } .back-button { -fx-background-color: white; diff --git a/src/main/resources/userpage/userpage.css b/src/main/resources/userpage/userpage.css index 8401a77..8d68841 100644 --- a/src/main/resources/userpage/userpage.css +++ b/src/main/resources/userpage/userpage.css @@ -1,31 +1,86 @@ #profile-name { - -fx-font-size: 28px; - -fx-font-weight: bold; + -fx-font-size: 28px; + -fx-font-weight: bold; } + .profile-info { - -fx-font-size: 16px; - -fx-text-fill: #444; + -fx-font-size: 16px; + -fx-text-fill: #444; } + .section-title { - -fx-font-size: 14px; - -fx-font-weight: bold; - -fx-fill: #888; + -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; + -fx-background-color: #ddd; + -fx-pref-height: 150px; + -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; + -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; + -fx-background-color: #c02020; +} + +.cause-chip { + -fx-background-color: #4CAF50; + -fx-text-fill: white; + -fx-padding: 6 12; + -fx-background-radius: 20; + -fx-font-size: 13px; +} + +.cause-chip:hover { + -fx-background-color: #45a049; + -fx-cursor: hand; +} + +.donation-card { + -fx-background-color: #f5f5f5; + -fx-border-color: #ddd; + -fx-border-radius: 6; + -fx-background-radius: 6; +} + +.donation-card:hover { + -fx-background-color: #efefef; + -fx-border-color: #bbb; +} + +.donation-org-name { + -fx-font-size: 15px; + -fx-font-weight: bold; + -fx-fill: #333; +} + +.donation-amount { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: #2196F3; +} + +.donation-date { + -fx-font-size: 12px; + -fx-text-fill: #999; +} + +.donation-list { + -fx-pref-height: 400px; +} + +/* ScrollPane styling */ +.scroll-pane { + -fx-control-inner-background: #fafafa; } \ 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 index 80d37bf..3ed1aec 100644 --- a/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java +++ b/src/test/java/edu/group5/app/model/donation/DonationServiceTest.java @@ -1,6 +1,7 @@ package edu.group5.app.model.donation; import edu.group5.app.model.organization.OrganizationRepository; +import edu.group5.app.model.organization.OrganizationScraper; import edu.group5.app.model.user.Customer; import org.junit.jupiter.api.BeforeEach; @@ -21,18 +22,20 @@ class DonationServiceTest { private OrganizationRepository organizationRepository; private DonationService donationService; private Customer customer; + private OrganizationScraper scraper; @BeforeEach void setUp() { + scraper = new OrganizationScraper(); HashMap orgMap = new HashMap<>(); - orgMap.put("org_number", "101"); + orgMap.put("org_number", "101"); orgMap.put("name", "CharityOrg"); - orgMap.put("status", "approved"); + orgMap.put("status", "approved"); orgMap.put("url", "https://charity.org"); - orgMap.put("is_pre_approved", true); + orgMap.put("is_pre_approved", true); Object[] orgInput = new Object[]{ orgMap }; - organizationRepository = new OrganizationRepository(orgInput); + organizationRepository = new OrganizationRepository(orgInput, scraper); donationRepository = new DonationRepository(new ArrayList<>()); diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java index f821e35..8906395 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationRepositoryTest.java @@ -10,9 +10,11 @@ class OrganizationRepositoryTest { private OrganizationRepository repository; + private OrganizationScraper scraper; @BeforeEach void setUp() { + scraper = new OrganizationScraper(); Object[] content = new Object[] { Map.of( "org_number", "1", @@ -43,13 +45,13 @@ void setUp() { "is_pre_approved", true ) }; - repository = new OrganizationRepository(content); + repository = new OrganizationRepository(content, scraper); } private void constructorTest(Object[] input, String expectedMessage) { IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, - () -> new OrganizationRepository(input) + () -> new OrganizationRepository(input, scraper) ); assertEquals(expectedMessage, exception.getMessage()); } @@ -58,6 +60,24 @@ void constructor_ThrowsWhenContentIsNull() { constructorTest(null, "The input cannot be null"); } + @Test + void constructor_ThrowsWhenScraperIsNull() { + Object[] content = new Object[] { + Map.of( + "org_number", "1", + "name", "Org", + "status", "approved", + "url", "org.com", + "is_pre_approved", true + ) + }; + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new OrganizationRepository(content, null) + ); + assertEquals("The scraper cannot be null", exception.getMessage()); + } + @Test void getTrustedOrganizations_OnlyReturnsTrustedOrganizations() { Map trusted = repository.getTrustedOrganizations(); @@ -128,7 +148,7 @@ void testExportAllOrganizations() { @Test void testExportAllOrganizationsThrowsWhenRepositoryIsEmpty() { - OrganizationRepository emptyRepo = new OrganizationRepository(new Object[0]); + OrganizationRepository emptyRepo = new OrganizationRepository(new Object[0], scraper); IllegalStateException exception = assertThrows( IllegalStateException.class, () -> emptyRepo.export() ); diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java new file mode 100644 index 0000000..c6c318b --- /dev/null +++ b/src/test/java/edu/group5/app/model/organization/OrganizationScraperTest.java @@ -0,0 +1,66 @@ +package edu.group5.app.model.organization; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class OrganizationScraperTest { + + private OrganizationScraper scraper; + + @BeforeEach + void setUp() { + scraper = new OrganizationScraper(); + } + + @Test + void fetchDescription_ReturnsNullWhenUrlIsNull() { + assertNull(scraper.fetchDescription(null)); + } + + @Test + void fetchDescription_ReturnsNullWhenUrlIsBlank() { + assertNull(scraper.fetchDescription("")); + } + + @Test + void fetchDescription_ReturnsNullWhenUrlIsInvalid() { + String result = scraper.fetchDescription("https://invalid-url-that-does-not-exist-xyz123.com"); + assertNull(result); + } + + @Test + void fetchDescription_CachesResultOnSecondCall() { + // Mock URLs won't work, but cache still works with null returns + scraper.fetchDescription("https://example.com"); + scraper.fetchDescription("https://example.com"); + // If no exception thrown, cache works + assertTrue(true); + } + + @Test + void fetchLogoUrl_ReturnsNullWhenUrlIsNull() { + assertNull(scraper.fetchLogoUrl(null)); + } + + @Test + void fetchLogoUrl_ReturnsNullWhenUrlIsBlank() { + assertNull(scraper.fetchLogoUrl("")); + } + + @Test + void fetchLogoUrl_ReturnsNullWhenUrlIsInvalid() { + String result = scraper.fetchLogoUrl("https://invalid-url-that-does-not-exist-xyz123.com"); + assertNull(result); + } + + @Test + void fetchLogoUrl_CachesResultOnSecondCall() { + // Mock URLs won't work, but cache still works with null returns + scraper.fetchLogoUrl("https://example.com"); + scraper.fetchLogoUrl("https://example.com"); + // If no exception thrown, cache works + assertTrue(true); + } +} diff --git a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java index 0920e67..2d76391 100644 --- a/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java +++ b/src/test/java/edu/group5/app/model/organization/OrganizationServiceTest.java @@ -6,14 +6,17 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; public class OrganizationServiceTest { private OrganizationRepository repo; private OrganizationService service; + private OrganizationScraper scraper; private Object[] content; @BeforeEach public void setUp() { + scraper = new OrganizationScraper(); Map orgMap = new HashMap<>(); orgMap.put("org_number", "1"); orgMap.put("name", "Misjonsalliansen"); @@ -22,22 +25,52 @@ public void setUp() { orgMap.put("is_pre_approved", false); content = new Object[]{orgMap}; - repo = new OrganizationRepository(content); - service = new OrganizationService(repo); + repo = new OrganizationRepository(content, scraper); + service = new OrganizationService(repo, scraper); } @Test - void constructor_throwsIfNull() { + void constructor_throwsIfRepositoryIsNull() { IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, - () -> new OrganizationService(null)); + () -> new OrganizationService(null, scraper)); assertEquals("OrganizationRepository cannot be null", ex.getMessage()); } + @Test + void constructor_throwsIfScraperIsNull() { + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new OrganizationService(repo, null)); + assertEquals("OrganizationScraper cannot be null", ex.getMessage()); + } + @Test void testGetOrganizationRepository() { assertEquals(repo, service.getOrganizationRepository()); } + @Test + void testGetTrustedOrganizationsWithLogos() { + Map orgsWithLogos = service.getTrustedOrganizationsWithLogos(); + assertNotNull(orgsWithLogos); + assertTrue(orgsWithLogos.containsKey(1)); + Organization org = orgsWithLogos.get(1); + assertEquals(1, org.orgNumber()); + assertEquals("Misjonsalliansen", org.name()); + assertNotNull(org); + } + + @Test + void testGetTrustedOrganizationsWithLogosAsync() throws Exception { + CompletableFuture> futureOrgs = + service.getTrustedOrganizationsWithLogosAsync(); + + assertNotNull(futureOrgs); + Map orgsWithLogos = futureOrgs.get(); + assertNotNull(orgsWithLogos); + assertTrue(orgsWithLogos.containsKey(1)); + assertEquals("Misjonsalliansen", orgsWithLogos.get(1).name()); + } + @Test void testGetTrustedOrganizations() { Map trustedOrgs = service.getTrustedOrganizations(); @@ -67,19 +100,5 @@ void testFindByOrgName() { assertEquals(1, org.orgNumber()); assertEquals("Misjonsalliansen", org.name()); } - - @Test - void fetchLogoUrlReturnsNullWhenUrlIsNull() { - assertNull(service.fetchLogoUrl(null)); - } - @Test - void fetchLogoUrlReturnsNullWhenUrlIsBlank() { - assertNull(service.fetchLogoUrl("")); - } - @Test - void fetchLogoUrlCachesResultOnSecondCall() { - String result1 = service.fetchLogoUrl("https://"); - String result2 = service.fetchLogoUrl("https://"); - assertEquals(result1, result2); - } } + diff --git a/src/test/java/edu/group5/app/utils/ParameterValidatorTest.java b/src/test/java/edu/group5/app/utils/ParameterValidatorTest.java new file mode 100644 index 0000000..bf8ac0c --- /dev/null +++ b/src/test/java/edu/group5/app/utils/ParameterValidatorTest.java @@ -0,0 +1,63 @@ +package edu.group5.app.utils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ParameterValidatorTest { + + @Test + void testValidatorDoesNotThrowWithValidParameters() { + assertDoesNotThrow(() -> ParameterValidator.stringChecker("valid", "validString")); + assertDoesNotThrow(() -> ParameterValidator.intChecker(1, "positiveInt")); + assertDoesNotThrow(() -> ParameterValidator.objectChecker(new Object(), "validObject")); + assertDoesNotThrow(() -> ParameterValidator.bigDecimalChecker(java.math.BigDecimal.valueOf(1), "positiveBigDecimal")); + } + + @Test + void testValidatorThrowsWithStringChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.stringChecker(null, "nullString"); + }); + IllegalArgumentException exception2 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.stringChecker("", "emptyString"); + }); + IllegalArgumentException exception3 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.stringChecker(" ", "blankString"); + }); + assertEquals("nullString can't be null", exception.getMessage()); + assertEquals("emptyString can't be blank", exception2.getMessage()); + assertEquals("blankString can't be blank", exception3.getMessage()); + } + + @Test + void testValidatorThrowsWithIntChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.intChecker(-1, "negativeInt"); + }); + assertEquals("negativeInt must be a positive integer", exception.getMessage()); + } + + @Test + void testValidatorThrowsWithObjectChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.objectChecker(null, "nullObject"); + }); + assertEquals("nullObject can't be null", exception.getMessage()); + } + + @Test + void testValidatorThrowsWithBigDecimalChecker() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.bigDecimalChecker(null, "nullBigDecimal"); + }); + IllegalArgumentException exception2 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.bigDecimalChecker(java.math.BigDecimal.valueOf(-1), "negativeBigDecimal"); + }); + IllegalArgumentException exception3 = assertThrows(IllegalArgumentException.class, () -> { + ParameterValidator.bigDecimalChecker(java.math.BigDecimal.ZERO, "zeroBigDecimal"); + }); + assertEquals("nullBigDecimal can't be null", exception.getMessage()); + assertEquals("negativeBigDecimal must be larger than 0", exception2.getMessage()); + assertEquals("zeroBigDecimal must be larger than 0", exception3.getMessage()); + } +}