diff --git a/givehope.db b/givehope.db deleted file mode 100644 index abc4973..0000000 Binary files a/givehope.db and /dev/null differ diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 907cc4a..7cc167d 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -1,34 +1,63 @@ package app; import application.security.PasswordHasher; -import application.user.UserLogin; import application.user.UserRegister; +import domain.donation.Donation; +import domain.organization.Organization; import domain.user.User; +import integration.InnsamlingskontrollenClient; import integration.security.Sha256PasswordHasher; +import java.math.BigDecimal; +import java.util.List; +import persistence.DonationDao; +import persistence.OrganizationDao; import persistence.UserDao; import persistence.UserRepository; import persistence.db.Database; public class Main { - UserDao userDao = new UserDao(); public static void main(String[] args) { - try{ + try { Database.getConnection(); System.out.println("Database connected"); + UserDao userDao = new UserDao(); + OrganizationDao orgDao = new OrganizationDao(); + DonationDao donationDao = new DonationDao(); + PasswordHasher hasher = new Sha256PasswordHasher(); UserRepository userRepo = new UserDao(); UserRegister userReg = new UserRegister(hasher, userRepo); - UserLogin login = new UserLogin(hasher, userRepo); + // Test brukerregistrering + userReg.execute("testuser", "12345678", "passord123", "test@example.com"); + System.out.println("Inserted user!"); - userReg.execute("wfrfrewfhbjdddswe3f", "45586456", "Jegerkddsdwdwful", "113@1h33weqd3.com"); + User user = userDao.findByEmail("test@example.com").orElseThrow(); + System.out.println("Hentet bruker: " + user.getUsername() + ", id=" + user.getID()); + + // Hent og lagre organisasjoner fra Innsamlingskontrollen + System.out.println("Henter organisasjoner fra Innsamlingskontrollen..."); + Organization[] fetchedOrgs = InnsamlingskontrollenClient.fetchOrganizations(); + for (Organization org : fetchedOrgs) { + orgDao.insert(org); + } + System.out.println("Lastet inn " + fetchedOrgs.length + " organisasjoner"); + + List orgs = orgDao.getAll(); + System.out.println("Organisasjoner i DB: " + orgs.size()); + System.out.println("Første org: " + orgs.get(0).getName() + ", org_number=" + orgs.get(0).getOrgNumber()); + + // Test donasjon med første org fra DB + Donation donation = new Donation(new BigDecimal("100.00"), user, orgs.get(0)); + donationDao.insert(donation); + System.out.println("Inserted donation, id=" + donation.getId()); + + List donations = donationDao.findByUser(user.getID()); + System.out.println("Antall donasjoner for bruker: " + donations.size()); - System.out.println("Inserted user!"); } catch (Exception e) { e.printStackTrace(); } - - } } diff --git a/src/main/java/application/user/UserStatistics.java b/src/main/java/application/user/UserStatistics.java new file mode 100644 index 0000000..5fe67d4 --- /dev/null +++ b/src/main/java/application/user/UserStatistics.java @@ -0,0 +1,69 @@ +package application.user; + +import domain.user.User; +import java.util.ArrayList; +import java.util.List; +import persistence.DonationDao; + +/** + * Provides statistical information about a user's donation activity. + * Acts as a service layer between the application and {@link DonationDao}, + * exposing user-friendly methods for retrieving donation history, + * total amounts, and favorite organizations. + */ +public class UserStatistics { + private DonationDao donationDao; + + /** + * Constructs a new {@code UserStatistics} instance with a default {@link DonationDao}. + */ + public UserStatistics() { + this.donationDao = new DonationDao(); + } + + /** + * Returns the top three organizations that the user has donated to most frequently. + * + * @param user the user whose favorite organizations are to be retrieved; must not be null + * @return a list of up to three formatted strings, each representing an organization + * and its donation count; empty list if the user has no donations + */ + public List userFavoriteOrganizations(User user) { + List favoriteOrganizations = donationDao.getFavoriteOrganizations(user.getID()); + return favoriteOrganizations; + } + + /** + * Returns a formatted list of all donations made by the user. + * Each entry contains the donation amount, date, and organization. + * + * @param user the user whose donation history is to be retrieved; must not be null + * @return a list of formatted donation strings; empty list if the user has no donations + */ + public List userDonations(User user) { + List userDonationList = donationDao.getUserDonations(user.getID()); + return userDonationList; + } + + /** + * Returns the total amount donated by the user across all donations. + * + * @param user the user whose total donation amount is to be calculated; must not be null + * @return a string representation of the total donated amount; {@code "0"} if no donations exist + */ + public String userTotalDonationAmount(User user) { + String totalDonations = donationDao.getTotalDonationAmount(user.getID()); + return totalDonations; + } + + /** + * Returns the total number of donations made by the user. + * + * @param user the user whose donation count is to be retrieved; must not be null + * @return a string representation of the total number of donations; {@code "0"} if none exist + */ + public String getTotalDonationsMade(User user) { + String totalDonationsMade = donationDao.getTotalDonationsMade(user.getID()); + return totalDonationsMade; + } +} diff --git a/src/main/java/domain/organization/Organization.java b/src/main/java/domain/organization/Organization.java index c4d5d3c..fd2cabf 100644 --- a/src/main/java/domain/organization/Organization.java +++ b/src/main/java/domain/organization/Organization.java @@ -1,81 +1,44 @@ package domain.organization; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import domain.donation.Cause; import java.util.Objects; +@JsonIgnoreProperties(ignoreUnknown = true) public class Organization { - private final String name; - private final String orgNr; - private final Cause cause; - private final String description; - private final String contactEmail; - private final String website; - private final boolean verified; + @JsonProperty("org_number") + private String orgNumber; - public Organization(String name, String orgNr, Cause cause, String description, String contactEmail, String website, boolean verified) { - if (name == null || name.isBlank()) { - throw new IllegalArgumentException("Name cannot be null or blank"); - } - if (orgNr == null || !orgNr.matches("\\d{9}")) { - throw new IllegalArgumentException("Organization number must be 9 digits"); - } - if (contactEmail != null) { - contactEmail = contactEmail.trim(); + @JsonProperty("name") + private String name; - if (contactEmail.isBlank()) { - throw new IllegalArgumentException("Email cannot be blank"); - } + @JsonProperty("status") + private String status; - if (!contactEmail.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" - )) //Got help from ChatGPT on regex - { - throw new IllegalArgumentException("Invalid email format"); - } - } + @JsonProperty("url") + private String url; - if (website != null) { - website = website.trim(); + @JsonProperty("is_pre_approved") + private boolean isPreApproved; - if (website.isBlank()) { - throw new IllegalArgumentException("Website cannot be blank"); - } + public Organization() {} - if (!website.matches("^https?://.+")) { //Got help from ChatGPT for regex - throw new IllegalArgumentException("Website must start with http:// or https://"); - } - } - this.name = name.trim(); - this.orgNr = orgNr.trim(); - this.cause = Objects.requireNonNull(cause, "Cause cannot be null"); - this.description = description; - this.contactEmail = contactEmail; - this.website = website; - this.verified = verified; - } + public String getOrgNumber() { return orgNumber; } + public void setOrgNumber(String orgNumber) { this.orgNumber = orgNumber; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } - public String getName() { - return name; - } - public String getOrgNr() { - return orgNr; - } - public Cause getCause() { - return cause; - } - public String getDescription() { - return description; - } - public String getContactEmail() { - return contactEmail; - } - public String getWebsite() { - return website; - } - public boolean isVerified() { - return verified; - } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getUrl() { return url; } + public void setUrl(String url) { this.url = url; } + + public boolean isPreApproved() { return isPreApproved; } + public void setPreApproved(boolean preApproved) { isPreApproved = preApproved; } } diff --git a/src/main/java/domain/user/User.java b/src/main/java/domain/user/User.java index 9b5e82f..ee6a2a2 100644 --- a/src/main/java/domain/user/User.java +++ b/src/main/java/domain/user/User.java @@ -5,6 +5,7 @@ public class User { private String phoneNumber; private final String eMail; private String password; + private Long id; public User(String userName, String phoneNumber, String password, String eMail) { @@ -28,6 +29,7 @@ public User(String userName, String phoneNumber, String password, String eMail) this.phoneNumber = phoneNumber; this.eMail = eMail.trim(); this.password = password; + this.id = id; } public String getUsername() { @@ -46,6 +48,8 @@ public String getPassword() { return password; } + public long getID() { return id; } + public void setPassword(String password) { this.password = password; } @@ -57,4 +61,9 @@ public void setUsername(String userName) { public void setPhonenumber (String phoneNumber) { this.phoneNumber = phoneNumber; } + + public void setId(Long id) { + if (this.id != null) throw new IllegalStateException("ID already set"); + this.id = id; + } } \ No newline at end of file diff --git a/src/main/java/persistence/DonationDao.java b/src/main/java/persistence/DonationDao.java index a1a0b46..0361f2b 100644 --- a/src/main/java/persistence/DonationDao.java +++ b/src/main/java/persistence/DonationDao.java @@ -1,4 +1,282 @@ package persistence; +import domain.donation.Donation; +import domain.organization.Organization; +import domain.user.User; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import persistence.db.Database; + +/** + * Data Access Object for donation-related database operations. + * Provides methods for inserting donations and retrieving donation statistics + * such as history, total amounts, and favorite organizations for a given user. + */ public class DonationDao { -} + /** + * Inserts a new donation record into the database. + * Upon successful insertion, the generated database ID is set on the donation object. + * + * @param donation the {@link Donation} to insert; must not be null and must have + * a valid amount, user, and organization + * @throws RuntimeException if a database error occurs during the insert + */ + public void insert(Donation donation) { + String sql = "INSERT INTO donation(amount, donation_date" + + ", user_id, organization_id) VALUES(?, ?, ?, ?)"; + + try (Connection conn = Database.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(sql, + Statement.RETURN_GENERATED_KEYS); + + stmt.setString(1, donation.getAmount().toString()); + stmt.setString(2, donation.getDateTime().toString()); + stmt.setLong(3, donation.getUser().getID()); + stmt.setString(4, donation.getOrganization().getOrgNumber()); + stmt.executeUpdate(); + + ResultSet keys = stmt.getGeneratedKeys(); + if (keys.next()) { + donation.setId(keys.getLong(1)); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Maps a single row from the provided {@code ResultSet} into a {@link Donation} object. + * The method extracts information about the donation, user, and organization + * from the {@code ResultSet} and constructs corresponding objects. + * + * @param rs the {@code ResultSet} containing the data for a single row; must not be null + * @return a {@code Donation} object populated with data from the row in the {@code ResultSet} + * @throws SQLException if a database access error occurs or if the column labels cannot be found + */ + private Donation mapRow(ResultSet rs) throws SQLException { + User user = new User( + rs.getString("user_name"), + rs.getString("phone_number"), + rs.getString("password_hash"), + rs.getString("e_mail") + ); + user.setId(rs.getLong("user_id")); + + Organization org = new Organization(); + org.setOrgNumber(rs.getString("org_number")); + org.setName(rs.getString("name")); + org.setStatus(rs.getString("status")); + org.setUrl(rs.getString("url")); + org.setPreApproved(rs.getInt("is_pre_approved") == 1); + + + Donation donation = new Donation( + new BigDecimal(rs.getString("amount")), + user, + org + ); + donation.setId(rs.getLong("id")); + return donation; + } + + /** + * Retrieves a list of donations made by a specific user, based on the user ID. + * The donations include details such as the donation amount, date, associated user, + * and organization. + * + * @param userId the unique identifier of the user whose donations are to be retrieved + * @return a list of {@code Donation} objects representing donations made by the user + * @throws RuntimeException if an error occurs during the database operation + */ + public List findByUser(long userId) { + String sql = """ + SELECT d.id, d.amount, d.donation_date, + u.id AS user_id, u.user_name, u.phone_number, u.e_mail, u.password_hash, + o.org_number, o.name, o.status, o.url, o.is_pre_approved + FROM donation d + JOIN user u ON d.user_id = u.id + JOIN organization o ON d.organization_id = o.org_number + WHERE d.user_id = ? + """; + + try (Connection conn = Database.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(sql); + + stmt.setLong(1, userId); + ResultSet rs = stmt.executeQuery(); + List list = new ArrayList<>(); + while (rs.next()) list.add(mapRow(rs)); + return list; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Retrieves a formatted list of donations made by a specific user. + * Each entry is a string on the form {@code "Amount: X, Date: Y, Organization: Z"}. + * + * @param userId the unique identifier of the user whose donations are to be retrieved + * @return a list of formatted strings, one per donation; empty list if none found + * @throws RuntimeException if a database error occurs during the query + */ + public List getUserDonations(long userId) { + String sql = """ + SELECT amount, donation_date, organization_id + FROM donation + WHERE user_Id = ? + """; + + try (Connection conn = Database.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(sql); + + stmt.setLong(1, userId); + ResultSet rs = stmt.executeQuery(); + List list = new ArrayList<>(); + while (rs.next()) list.add(mapRowSimple(rs)); + return list; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Retrieves a list of the user's favorite organizations based on donation history. + * The method queries the database to find the organizations the user has donated to most frequently + * and limits the result to the top three organizations. + * + * @param userId the unique identifier of the user whose favorite organizations are to be retrieved + * @return a list of strings representing the top three organizations donated to by the user, + * ordered by the number of donations in descending order + * @throws RuntimeException if an error occurs while querying the database + */ + public List getFavoriteOrganizations(long userId) { + String sql = """ + SELECT organization_id, COUNT(*) AS donation_count + FROM donation + WHERE user_Id = ? + GROUP BY organization_id + ORDER BY donation_count DESC + LIMIT 3 + """; + + try (Connection conn = Database.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(sql); + + stmt.setLong(1, userId); + ResultSet rs = stmt.executeQuery(); + List list = new ArrayList<>(); + while (rs.next()) list.add(mapRowFavorite(rs)); + return list; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Retrieves the total donation amount made by a specific user. + * The method calculates the sum of all donations associated with the given user ID. + * + * @param userId the unique identifier of the user whose total donation amount is to be retrieved + * @return a string representing the total donation amount made by the user; if no donations exist, + * the method returns "0" + * @throws RuntimeException if an error occurs during database access + */ + public String getTotalDonationAmount(long userId) { + String sql = """ + SELECT SUM(CAST(amount as REAL)) + FROM donation + WHERE user_id = ? + """; + + try (Connection conn = Database.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(sql); + + stmt.setLong(1, userId); + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + BigDecimal amount = rs.getBigDecimal(1); + return amount.toString(); + } + return "0"; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Retrieves the total number of donations made by a specific user. + * The method counts the number of donation records associated with the provided user ID + * in the database and returns the total count as a string. + * + * @param userId the unique identifier of the user whose total number of donations + * is to be retrieved + * @return a string representing the total count of donations made by the user; + * returns "0" if no donations exist + * @throws RuntimeException if a database access error occurs + */ + public String getTotalDonationsMade(long userId) { + String sql = """ + SELECT COUNT(*) + FROM donation + WHERE user_id = ? + """; + + try (Connection conn = Database.getConnection()) { + PreparedStatement stmt = conn.prepareStatement(sql); + + stmt.setLong(1, userId); + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + int amount = rs.getInt(1); + String s = String.valueOf(amount); + return s; + } + return "0"; + } catch (SQLException e) { + throw new RuntimeException(e); + } + + } + + /** + * Maps a single row from the provided {@code ResultSet} into a string representation + * consisting of the donation amount, date, and organization ID. + * + * @param rs the {@code ResultSet} containing the data for a single row; must not be null + * @return a string representing the donation with details such as amount, date, + * and organization ID + * @throws SQLException if a database access error occurs or if the column labels cannot be found + */ + private String mapRowSimple(ResultSet rs) throws SQLException { + return "Amount: " + rs.getString("amount") + + ", Date: " + rs.getString("donation_date") + + ", Organization: " + rs.getString("organization_id"); + } + + /** + * Maps a single row from the provided {@code ResultSet} into a formatted string + * combining the organization ID and its donation count. + * + * @param rs the {@code ResultSet} containing the data for a single row; must not be null + * @return a string on the form {@code "organization_id (N donations)"} + * @throws SQLException if a database access error occurs or a column label cannot be found + */ + private String mapRowFavorite(ResultSet rs) throws SQLException { + return rs.getString("organization_id") + + " (" + rs.getInt("donation_count") + " donasjoner)"; + } + + + + + + + +} \ No newline at end of file diff --git a/src/main/java/persistence/OrganizationDao.java b/src/main/java/persistence/OrganizationDao.java index 8b45a05..a0d1346 100644 --- a/src/main/java/persistence/OrganizationDao.java +++ b/src/main/java/persistence/OrganizationDao.java @@ -1,4 +1,52 @@ package persistence; +import domain.organization.Organization; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + public class OrganizationDao { -} + + private static final String URL = "jdbc:sqlite:givehope.db"; + + public void insert(Organization org) throws SQLException { + String sql = """ + INSERT OR IGNORE INTO organization + (org_number, name, status, url, is_pre_approved) + VALUES (?, ?, ?, ?, ?) + """; + + try (Connection conn = DriverManager.getConnection(URL); + PreparedStatement stmt = conn.prepareStatement(sql)) { + + stmt.setString(1, org.getOrgNumber()); + stmt.setString(2, org.getName()); + stmt.setString(3, org.getStatus()); + stmt.setString(4, org.getUrl()); + stmt.setInt(5, org.isPreApproved() ? 1 : 0); + stmt.executeUpdate(); + } + } + + public List getAll() throws SQLException { + String sql = "SELECT * FROM organization"; + List list = new ArrayList<>(); + + try (Connection conn = DriverManager.getConnection(URL); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + + while (rs.next()) { + Organization org = new Organization(); + org.setOrgNumber(rs.getString("org_number")); + org.setName(rs.getString("name")); + org.setStatus(rs.getString("status")); + org.setUrl(rs.getString("url")); + org.setPreApproved(rs.getInt("is_pre_approved") == 1); + list.add(org); + } + } + return list; + } +} \ No newline at end of file diff --git a/src/main/java/persistence/UserDao.java b/src/main/java/persistence/UserDao.java index 7e4ceaf..2fa7765 100644 --- a/src/main/java/persistence/UserDao.java +++ b/src/main/java/persistence/UserDao.java @@ -1,6 +1,7 @@ package persistence; import java.sql.ResultSet; +import java.sql.Statement; import java.util.Optional; import persistence.db.Database; import java.sql.Connection; @@ -15,7 +16,8 @@ public void insert(User user) { String sql = "INSERT INTO user(user_name, phone_number, e_mail, password_hash) VALUES(?, ?, ?, ?)"; try (Connection conn = Database.getConnection()) { - PreparedStatement stmt = conn.prepareStatement(sql); + PreparedStatement stmt = conn.prepareStatement(sql, + Statement.RETURN_GENERATED_KEYS); stmt.setString(1, user.getUsername()); stmt.setString(2, user.getPhoneNumber()); @@ -25,6 +27,11 @@ public void insert(User user) { stmt.executeUpdate(); + ResultSet keys = stmt.getGeneratedKeys(); + if (keys.next()) { + user.setId(keys.getLong(1)); + } + } catch (SQLException e) { throw new RuntimeException(e); } @@ -35,7 +42,7 @@ public void insert(User user) { @Override public Optional findByEmail(String email) { String sql = """ - SELECT user_name, phone_number, e_mail, password_hash + SELECT id, user_name, phone_number, e_mail, password_hash FROM user WHERE e_mail = ? LIMIT 1 @@ -63,7 +70,7 @@ public Optional findByEmail(String email) { @Override public Optional findByUsername(String username) { String sql = """ - SELECT user_name, phone_number, e_mail, password_hash + SELECT id, user_name, phone_number, e_mail, password_hash FROM user WHERE user_name = ? LIMIT 1 @@ -153,8 +160,9 @@ private User mapUser(ResultSet rs) throws SQLException { String phone = rs.getString("phone_number"); String email = rs.getString("e_mail"); String passwordHash = rs.getString("password_hash"); - - return new User(username, phone, passwordHash, email); + User user = new User(username, phone, passwordHash, email); + user.setId(rs.getLong("id")); + return user; } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 9975777..5288a7e 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,35 +1,26 @@ -PRAGMA foreign_keys = ON; - CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY AUTOINCREMENT, - user_name TEXT NOT NULL, + user_name TEXT UNIQUE NOT NULL, phone_number TEXT, e_mail TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS organization ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + org_number TEXT PRIMARY KEY, name TEXT NOT NULL, - description TEXT -); - -CREATE TABLE IF NOT EXISTS cause ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - description TEXT, - organization_id INTEGER NOT NULL, - FOREIGN KEY (organization_id) REFERENCES organization(id) - ON DELETE CASCADE + status TEXT, + url TEXT, + is_pre_approved INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS donation ( id INTEGER PRIMARY KEY AUTOINCREMENT, - amount REAL NOT NULL, + amount TEXT NOT NULL, donation_date TEXT NOT NULL, - user_id INTEGER, - cause_id INTEGER NOT NULL, - FOREIGN KEY (user_id) REFERENCES user(id), - FOREIGN KEY (cause_id) REFERENCES cause(id) -); \ No newline at end of file + user_id INTEGER NOT NULL, + organization_id TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE RESTRICT, + FOREIGN KEY (organization_id) REFERENCES organization(org_number) ON DELETE RESTRICT + ); \ No newline at end of file