diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml
new file mode 100644
index 0000000..0aa927d
--- /dev/null
+++ b/.gitea/workflows/build.yml
@@ -0,0 +1,54 @@
+name: Build and Deploy Spring Boot Server
+
+on:
+ push:
+ branches:
+ - dev
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ # 1. Checkout the code
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ # 2. Set up Java and Maven
+ - name: Set up JDK (Eclipse Temurin)
+ uses: actions/setup-java@v3
+ with:
+ distribution: "temurin"
+ java-version: "17"
+ cache: maven
+
+ # 3. Verify Maven installation
+ - name: Install Maven
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y maven
+ mvn -version
+
+ # 4. Build the Spring Boot application
+ - name: Build Spring Boot Application
+ run: |
+ mvn clean package -DskipTests
+
+ # 5. Set up Docker
+ - name: Set up Docker
+ run: |
+ docker --version
+
+ # 6. Build the Docker image
+ - name: Build and Package Docker Image
+ run: |
+ docker build -t tea.zendric.de/cedric/xpensely-server:latest .
+
+ # 7. Docker login
+ - name: Login to Docker Registry
+ run: |
+ echo "${{ secrets.TEAPASSWORD }}" | docker login tea.zendric.de -u ${{ secrets.TEAUSER }} --password-stdin
+ # 8. Push Docker image
+ - name: Push the Docker Image to registry
+ run: |
+ docker push tea.zendric.de/cedric/xpensely-server:latest
diff --git a/.gitea/workflows/tag_release.yml b/.gitea/workflows/tag_release.yml
new file mode 100644
index 0000000..7a4092c
--- /dev/null
+++ b/.gitea/workflows/tag_release.yml
@@ -0,0 +1,62 @@
+name: Build and Deploy Versioned Spring Boot Server
+
+on:
+ push:
+ tags: # Match all tags
+ - "*"
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ # 1. Checkout the code
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ # 2. Set up Java and Maven
+ - name: Set up JDK (Eclipse Temurin)
+ uses: actions/setup-java@v3
+ with:
+ distribution: "temurin"
+ java-version: "17"
+ cache: maven
+
+ # 3. Verify Maven installation
+ - name: Install Maven
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y maven
+ mvn -version
+
+ # 4. Build the Spring Boot application
+ - name: Build Spring Boot Application
+ run: |
+ mvn clean package -DskipTests
+
+ # 5. Set up Docker
+ - name: Set up Docker
+ run: |
+ docker --version
+
+ # 6. Extract the tag name
+ - name: Extract Tag Version
+ id: extract_version
+ run: |
+ TAG_VERSION=${GITEA_REF#refs/tags/}
+ echo "TAG_VERSION=$TAG_VERSION" >> $GITHUB_ENV
+
+ # 7. Build the Docker image with the tag
+ - name: Build and Package Docker Image
+ run: |
+ docker build -t tea.zendric.de/cedric/xpensely-server:${{ env.TAG_VERSION }} .
+
+ # 8. Docker login
+ - name: Login to Docker Registry
+ run: |
+ echo "${{ secrets.TEAPASSWORD }}" | docker login tea.zendric.de -u ${{ secrets.TEAUSER }} --password-stdin
+
+ # 9. Push the Docker image with the tag
+ - name: Push the Docker Image to registry
+ run: |
+ docker push tea.zendric.de/cedric/xpensely-server:${{ env.TAG_VERSION }}
diff --git a/.gitignore b/.gitignore
index f8185d3..37fdfda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
+.env
### STS ###
.apt_generated
diff --git a/apiDesign.drawio b/apiDesign.drawio
new file mode 100644
index 0000000..e276b67
--- /dev/null
+++ b/apiDesign.drawio
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..886656c
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,42 @@
+version: "3.8"
+services:
+ xpensely-server:
+ image: tea.zendric.de/cedric/xpensely-server:latest
+ container_name: xpensely-server
+ ports:
+ - 3636:8080
+ environment:
+ GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
+ GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
+ DB_PORT: 5434
+ DB_NAME: ${DB_P_NAME}
+ DB_USERNAME: ${DB_USERNAME}
+ DB_PASSWORD: ${DB_PASSWORD}
+ depends_on:
+ postgresdb:
+ condition: service_healthy
+ networks:
+ - xpensely-network
+ postgresdb:
+ image: postgres:14
+ container_name: postgresdb
+ ports:
+ - 5434:5432
+ environment:
+ POSTGRES_DB: ${DB_P_NAME}
+ POSTGRES_USER: ${DB_USERNAME}
+ POSTGRES_PASSWORD: ${DB_PASSWORD}
+ networks:
+ - xpensely-network
+ volumes:
+ - db_data:/var/lib/postgresql/data
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME}"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+volumes:
+ db_data: null
+networks:
+ xpensely-network: null
diff --git a/dockerfile b/dockerfile
new file mode 100644
index 0000000..f7e9a76
--- /dev/null
+++ b/dockerfile
@@ -0,0 +1,7 @@
+FROM openjdk:17-jdk-slim
+
+COPY ./target/*.jar app.jar
+
+EXPOSE 8080
+
+ENTRYPOINT ["java", "-jar", "app.jar"]
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index fdac1ff..df740a9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,9 +10,9 @@
de.zendric.app
XpenselyServer
- 0.0.1-SNAPSHOT
+ 1.0.0
XpenselyServer
- Demo project for Spring Boot
+ XpenselyServer used to handle the Xpensely App
@@ -38,11 +38,18 @@
org.springframework.boot
spring-boot-starter-security
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
org.springframework.boot
spring-boot-starter-web
-
org.springframework.boot
spring-boot-devtools
diff --git a/src/main/java/de/zendric/app/XpenselyServer/XpenselyServerApplication.java b/src/main/java/de/zendric/app/xpensely_server/XpenselyServerApplication.java
similarity index 70%
rename from src/main/java/de/zendric/app/XpenselyServer/XpenselyServerApplication.java
rename to src/main/java/de/zendric/app/xpensely_server/XpenselyServerApplication.java
index 0fe260e..ae78652 100644
--- a/src/main/java/de/zendric/app/XpenselyServer/XpenselyServerApplication.java
+++ b/src/main/java/de/zendric/app/xpensely_server/XpenselyServerApplication.java
@@ -1,9 +1,11 @@
-package de.zendric.app.XpenselyServer;
+package de.zendric.app.xpensely_server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
+@EnableScheduling
public class XpenselyServerApplication {
public static void main(String[] args) {
diff --git a/src/main/java/de/zendric/app/xpensely_server/controller/AppUserController.java b/src/main/java/de/zendric/app/xpensely_server/controller/AppUserController.java
new file mode 100644
index 0000000..e0b9994
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/controller/AppUserController.java
@@ -0,0 +1,73 @@
+package de.zendric.app.xpensely_server.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import de.zendric.app.xpensely_server.model.AppUser;
+import de.zendric.app.xpensely_server.model.AppUserCreateRequest;
+import de.zendric.app.xpensely_server.model.UsernameAlreadyExistsException;
+import de.zendric.app.xpensely_server.services.UserService;
+
+@RestController
+@RequestMapping("/api/users")
+public class AppUserController {
+
+ private UserService userService;
+
+ @Autowired
+ public AppUserController(UserService userService) {
+ this.userService = userService;
+ }
+
+ @GetMapping
+ public AppUser getUser(@RequestParam Long id) {
+ return userService.getUser(id);
+ }
+
+ @GetMapping("/byName")
+ public AppUser getUserByName(@RequestParam String username) {
+ return userService.getUserByName(username);
+ }
+
+ @GetMapping("/byGoogleId")
+ public ResponseEntity getUserByGoogleId(@RequestParam String id) {
+ try {
+ AppUser userByGoogleId = userService.getUserByGoogleId(id);
+ return new ResponseEntity<>(userByGoogleId, HttpStatus.OK);
+
+ } catch (IllegalArgumentException e) {
+ return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @PostMapping("/createUser")
+ public ResponseEntity createUser(@RequestBody AppUserCreateRequest userRequest) {
+ try {
+ AppUser convertedUser = userRequest.convertToAppUser();
+
+ AppUser nUser = userService.createUser(convertedUser);
+ return new ResponseEntity<>(nUser, HttpStatus.CREATED);
+ } catch (UsernameAlreadyExistsException e) {
+ return new ResponseEntity<>(null, HttpStatus.CONFLICT);
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
+ }
+
+ }
+
+ @DeleteMapping
+ public String deleteUser(@RequestParam Long id) {
+ AppUser user = userService.deleteUserById(id);
+ return "User deleted : " + user.getUsername();
+ }
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/controller/ExpenseListController.java b/src/main/java/de/zendric/app/xpensely_server/controller/ExpenseListController.java
new file mode 100644
index 0000000..3a407fb
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/controller/ExpenseListController.java
@@ -0,0 +1,212 @@
+package de.zendric.app.xpensely_server.controller;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import de.zendric.app.xpensely_server.model.AppUser;
+import de.zendric.app.xpensely_server.model.Expense;
+import de.zendric.app.xpensely_server.model.ExpenseChangeRequest;
+import de.zendric.app.xpensely_server.model.ExpenseInput;
+import de.zendric.app.xpensely_server.model.ExpenseList;
+import de.zendric.app.xpensely_server.model.InviteRequest;
+import de.zendric.app.xpensely_server.services.ExpenseListService;
+import de.zendric.app.xpensely_server.services.UserService;
+
+@RestController
+@RequestMapping("/api/expenselist")
+class ExpenseListController {
+
+ private ExpenseListService expenseListService;
+ private UserService userService;
+
+ @Autowired
+ public ExpenseListController(ExpenseListService expenseListService, UserService userService) {
+ this.expenseListService = expenseListService;
+ this.userService = userService;
+ }
+
+ @GetMapping("/all")
+ public ResponseEntity> getAll() {
+ try {
+ List items = new ArrayList<>();
+
+ expenseListService.findAll().forEach(items::add);
+
+ if (items.isEmpty())
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+
+ return new ResponseEntity<>(items, HttpStatus.OK);
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @GetMapping("/byUser")
+ public ResponseEntity> getByUser(@RequestParam Long userId) {
+ try {
+ List items = expenseListService.findByUserId(userId);
+
+ if (items.isEmpty())
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+
+ return new ResponseEntity<>(items, HttpStatus.OK);
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @GetMapping("/byUsername")
+ public ResponseEntity> getByUser(@RequestParam String username) {
+ try {
+ List items = expenseListService.findByUsername(username);
+
+ if (items.isEmpty())
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+
+ return new ResponseEntity<>(items, HttpStatus.OK);
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @GetMapping("/byId")
+ public ResponseEntity getById(@RequestParam Long id) {
+ Optional existingItemOptional = expenseListService.findById(id);
+
+ if (existingItemOptional.isPresent()) {
+ return new ResponseEntity<>(existingItemOptional.get(), HttpStatus.OK);
+ } else {
+ return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ }
+ }
+
+ @PostMapping("/create")
+ public ResponseEntity create(@RequestBody ExpenseList expenseList) {
+ try {
+ if (expenseList.getOwner() != null) {
+ AppUser existingOwner = userService.getUser(expenseList.getOwner().getId());
+ if (existingOwner == null) {
+ throw new IllegalArgumentException("Owner does not exist.");
+ }
+ expenseList.setOwner(existingOwner);
+ } else {
+ throw new IllegalArgumentException("Owner is required.");
+ }
+
+ expenseList.setSharedWith(null);
+
+ ExpenseList savedItem = expenseListService.createList(expenseList);
+ return new ResponseEntity<>(savedItem, HttpStatus.CREATED);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return new ResponseEntity<>(null, HttpStatus.EXPECTATION_FAILED);
+ }
+ }
+
+ @DeleteMapping("{id}")
+ public ResponseEntity delete(@PathVariable("id") Long id) {
+ try {
+ expenseListService.deleteById(id);
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ } catch (Exception e) {
+ return new ResponseEntity<>(HttpStatus.EXPECTATION_FAILED);
+ }
+ }
+
+ @PostMapping("/{id}/add")
+ public ResponseEntity addExpenseToList(
+ @PathVariable("id") Long expenseListId,
+ @RequestBody ExpenseInput expenseInput) {
+ try {
+ AppUser expenseOwner = userService.getUserByName(expenseInput.getOwner());
+ Expense expense = expenseInput.convertToExpense(expenseOwner.getId());
+
+ Expense addedExpense = expenseListService.addExpenseToList(expenseListId, expense);
+ return new ResponseEntity<>(addedExpense, HttpStatus.CREATED);
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+ }
+
+ @PutMapping("/{id}/update")
+ public ResponseEntity updateExpenseInList(
+ @PathVariable("id") Long expenseListId,
+ @RequestBody ExpenseChangeRequest expenseChangeRequest) {
+ try {
+ AppUser expenseOwner = userService.getUserByName(expenseChangeRequest.getOwnerName());
+ Optional expenseList = expenseListService.findById(expenseListId);
+ if (expenseList.isPresent()) {
+ Expense expense = expenseChangeRequest.convertToExpense(expenseOwner.getId(), expenseList.get());
+
+ Expense addedExpense = expenseListService.updateExpense(expenseListId, expense);
+ return new ResponseEntity<>(addedExpense, HttpStatus.CREATED);
+ }
+ return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
+
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
+ }
+ }
+
+ @DeleteMapping("/{id}/delete")
+ public ResponseEntity deleteExpenseFromList(
+ @PathVariable("id") Long expenseListId,
+ @RequestParam("expenseId") Long expenseId) {
+ try {
+ expenseListService.deleteExpenseFromList(expenseListId, expenseId);
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ } catch (Exception e) {
+ return new ResponseEntity<>(null, HttpStatus.EXPECTATION_FAILED);
+ }
+ }
+
+ @PostMapping("/{listId}/invite")
+ public ResponseEntity generateInvite(@PathVariable Long listId) {
+ String inviteCode = expenseListService.generateInviteCode(listId);
+ return ResponseEntity.ok(inviteCode);
+ }
+
+ @PostMapping("/accept-invite")
+ public ResponseEntity> acceptInvite(@RequestBody InviteRequest inviteRequest) {
+ ExpenseList list = expenseListService.findByInviteCode(inviteRequest.getInviteCode());
+
+ if (list == null || list.getInviteCodeExpiration() == null ||
+ list.getInviteCodeExpiration().isBefore(LocalDateTime.now())) {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Invalid or expired invite code");
+ }
+ if (list.getSharedWith() != null) {
+ return ResponseEntity.status(HttpStatus.IM_USED).body("List has already been shared");
+ }
+ if (list.getOwner().getId() == inviteRequest.getUserId()) {
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("You cant join your own List");
+ }
+ AppUser user = null;
+ try {
+ user = userService.getUser(inviteRequest.getUserId());
+ } catch (Exception e) {
+ throw new RuntimeException("User not found");
+ }
+ if (user != null) {
+ list.setSharedWith(user);
+ expenseListService.save(list);
+ } else {
+ throw new RuntimeException("User not found");
+ }
+ return ResponseEntity.ok("User added to the list");
+ }
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/controller/HomeController.java b/src/main/java/de/zendric/app/xpensely_server/controller/HomeController.java
new file mode 100644
index 0000000..12a8948
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/controller/HomeController.java
@@ -0,0 +1,13 @@
+package de.zendric.app.xpensely_server.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+class HomeController {
+
+ @GetMapping("/")
+ public String getAll() {
+ return "Welcome";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/zendric/app/xpensely_server/model/AppUser.java b/src/main/java/de/zendric/app/xpensely_server/model/AppUser.java
new file mode 100644
index 0000000..90a0cd9
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/model/AppUser.java
@@ -0,0 +1,40 @@
+package de.zendric.app.xpensely_server.model;
+
+import java.time.LocalDateTime;
+
+import org.hibernate.annotations.CreationTimestamp;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@Entity
+@EqualsAndHashCode
+public class AppUser {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "username", nullable = false, unique = true)
+ private String username;
+
+ @JsonIgnore
+ private String googleId;
+
+ @Column(updatable = false)
+ @CreationTimestamp
+ @JsonIgnore
+ private LocalDateTime createdAt;
+
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/model/AppUserCreateRequest.java b/src/main/java/de/zendric/app/xpensely_server/model/AppUserCreateRequest.java
new file mode 100644
index 0000000..3ae678d
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/model/AppUserCreateRequest.java
@@ -0,0 +1,21 @@
+package de.zendric.app.xpensely_server.model;
+
+import jakarta.persistence.Column;
+import lombok.Data;
+
+@Data
+public class AppUserCreateRequest {
+
+ @Column(name = "username", nullable = false, unique = true)
+ private String username;
+
+ private String googleId;
+
+ public AppUser convertToAppUser() {
+ AppUser appUser = new AppUser();
+ appUser.setGoogleId(googleId);
+ appUser.setUsername(username);
+
+ return appUser;
+ }
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/model/Expense.java b/src/main/java/de/zendric/app/xpensely_server/model/Expense.java
new file mode 100644
index 0000000..ab7bab7
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/model/Expense.java
@@ -0,0 +1,44 @@
+package de.zendric.app.xpensely_server.model;
+
+import java.time.LocalDate;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+public class Expense {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String title;
+
+ @ManyToOne
+ private AppUser owner;
+
+ private Double amount;
+ private Double personalUseAmount;
+ private Double otherPersonAmount;
+
+ private LocalDate date;
+
+ @ManyToOne
+ @JoinColumn(name = "expense_list_id", nullable = false)
+ @JsonBackReference
+ private ExpenseList expenseList;
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/model/ExpenseChangeRequest.java b/src/main/java/de/zendric/app/xpensely_server/model/ExpenseChangeRequest.java
new file mode 100644
index 0000000..c028b00
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/model/ExpenseChangeRequest.java
@@ -0,0 +1,39 @@
+package de.zendric.app.xpensely_server.model;
+
+import java.time.LocalDate;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ExpenseChangeRequest {
+
+ private Long id;
+ private String title;
+ private String ownerName;
+ private Double amount;
+ private Double personalUseAmount;
+ private Double otherPersonAmount;
+ private LocalDate date;
+
+ public Expense convertToExpense(Long userId, ExpenseList expenseList) {
+ AppUser appUser = new AppUser();
+ appUser.setId(userId);
+ appUser.setUsername(ownerName);
+
+ Expense expense = new Expense();
+ expense.setAmount(amount);
+ expense.setDate(date);
+ expense.setPersonalUseAmount(personalUseAmount);
+ expense.setOtherPersonAmount(otherPersonAmount);
+ expense.setExpenseList(expenseList);
+ expense.setId(id);
+ expense.setOwner(appUser);
+ expense.setTitle(title);
+
+ return expense;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/zendric/app/xpensely_server/model/ExpenseInput.java b/src/main/java/de/zendric/app/xpensely_server/model/ExpenseInput.java
new file mode 100644
index 0000000..ac2e7a6
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/model/ExpenseInput.java
@@ -0,0 +1,52 @@
+package de.zendric.app.xpensely_server.model;
+
+import java.time.LocalDate;
+
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+public class ExpenseInput {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String title;
+
+ private String owner;
+
+ private Double amount;
+ private Double personalUseAmount;
+ private Double otherPersonAmount;
+
+ private LocalDate date;
+
+ private ExpenseList expenseList;
+
+ public Expense convertToExpense(Long userId) {
+ AppUser appUser = new AppUser();
+ appUser.setId(userId);
+ appUser.setUsername(owner);
+
+ Expense expense = new Expense();
+ expense.setAmount(amount);
+ expense.setDate(date);
+ expense.setPersonalUseAmount(personalUseAmount);
+ expense.setOtherPersonAmount(otherPersonAmount);
+ expense.setExpenseList(expenseList);
+ expense.setId(id);
+ expense.setOwner(appUser);
+ expense.setTitle(title);
+
+ return expense;
+ }
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/model/ExpenseList.java b/src/main/java/de/zendric/app/xpensely_server/model/ExpenseList.java
new file mode 100644
index 0000000..a31f9de
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/model/ExpenseList.java
@@ -0,0 +1,58 @@
+package de.zendric.app.xpensely_server.model;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+public class ExpenseList {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String name;
+
+ private String inviteCode;
+ @JsonIgnore
+ private LocalDateTime inviteCodeExpiration;
+
+ @ManyToOne
+ private AppUser owner;
+
+ @ManyToOne
+ private AppUser sharedWith;
+
+ @OneToMany(mappedBy = "expenseList", cascade = CascadeType.ALL, orphanRemoval = true)
+ @JsonManagedReference
+ @jakarta.persistence.OrderBy("date ASC, id ASC")
+ private List expenses;
+
+ public void addExpense(Expense expense) {
+ expense.setExpenseList(this);
+ expenses.add(expense);
+
+ }
+
+ public void removeExpense(Expense expense) {
+ expenses.remove(expense);
+ expense.setExpenseList(null);
+ }
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/model/InviteRequest.java b/src/main/java/de/zendric/app/xpensely_server/model/InviteRequest.java
new file mode 100644
index 0000000..81ac35a
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/model/InviteRequest.java
@@ -0,0 +1,13 @@
+package de.zendric.app.xpensely_server.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class InviteRequest {
+ private String inviteCode;
+ private Long userId;
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/model/UsernameAlreadyExistsException.java b/src/main/java/de/zendric/app/xpensely_server/model/UsernameAlreadyExistsException.java
new file mode 100644
index 0000000..b0645d8
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/model/UsernameAlreadyExistsException.java
@@ -0,0 +1,11 @@
+package de.zendric.app.xpensely_server.model;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.CONFLICT)
+public class UsernameAlreadyExistsException extends RuntimeException {
+ public UsernameAlreadyExistsException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/zendric/app/xpensely_server/repo/ExpenseListRepository.java b/src/main/java/de/zendric/app/xpensely_server/repo/ExpenseListRepository.java
new file mode 100644
index 0000000..c42b820
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/repo/ExpenseListRepository.java
@@ -0,0 +1,15 @@
+package de.zendric.app.xpensely_server.repo;
+
+import java.util.List;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import de.zendric.app.xpensely_server.model.ExpenseList;
+
+@Repository
+public interface ExpenseListRepository extends JpaRepository {
+ List findByOwnerId(Long ownerId);
+
+ ExpenseList findByInviteCode(String inviteCode);
+}
\ No newline at end of file
diff --git a/src/main/java/de/zendric/app/xpensely_server/repo/ExpenseRepository.java b/src/main/java/de/zendric/app/xpensely_server/repo/ExpenseRepository.java
new file mode 100644
index 0000000..fe7a610
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/repo/ExpenseRepository.java
@@ -0,0 +1,13 @@
+package de.zendric.app.xpensely_server.repo;
+
+import java.util.List;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import de.zendric.app.xpensely_server.model.Expense;
+
+@Repository
+public interface ExpenseRepository extends JpaRepository {
+ List findAllByOrderByDateAsc();
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/repo/UserRepository.java b/src/main/java/de/zendric/app/xpensely_server/repo/UserRepository.java
new file mode 100644
index 0000000..a35ba4f
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/repo/UserRepository.java
@@ -0,0 +1,17 @@
+package de.zendric.app.xpensely_server.repo;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import de.zendric.app.xpensely_server.model.AppUser;
+
+@Repository
+public interface UserRepository extends JpaRepository {
+ Optional findByUsername(String username);
+
+ Optional findByGoogleId(String id);
+
+ Boolean existsByUsername(String username);
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/security/SecurityConfig.java b/src/main/java/de/zendric/app/xpensely_server/security/SecurityConfig.java
new file mode 100644
index 0000000..b4883a3
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/security/SecurityConfig.java
@@ -0,0 +1,35 @@
+package de.zendric.app.xpensely_server.security;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+ // @Bean
+ // public SecurityFilterChain securityFilterChain(HttpSecurity http) throws
+ // Exception {
+ // http.authorizeHttpRequests(auth -> auth
+ // .anyRequest().permitAll()).csrf().disable();
+ // ;
+
+ // return http.build();
+ // }
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .authorizeHttpRequests(auth -> auth
+ .anyRequest().authenticated())
+ .oauth2ResourceServer(oauth2 -> oauth2
+ .jwt(Customizer.withDefaults()))
+ .oauth2Login(Customizer.withDefaults())
+ .csrf().disable();
+
+ return http.build();
+ }
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/services/CleanupService.java b/src/main/java/de/zendric/app/xpensely_server/services/CleanupService.java
new file mode 100644
index 0000000..cbfd06c
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/services/CleanupService.java
@@ -0,0 +1,34 @@
+package de.zendric.app.xpensely_server.services;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import de.zendric.app.xpensely_server.model.ExpenseList;
+import de.zendric.app.xpensely_server.repo.ExpenseListRepository;
+
+@Service
+
+public class CleanupService {
+
+ @Autowired
+ private ExpenseListRepository expenseListRepository;
+
+ @Scheduled(cron = "0 0 0 * * ?") // Runs daily at midnight
+ public void cleanupExpiredInvites() {
+ List expiredLists = expenseListRepository.findAll().stream()
+ .filter(list -> list.getInviteCodeExpiration() != null &&
+ list.getInviteCodeExpiration().isBefore(LocalDateTime.now()))
+ .collect(Collectors.toList());
+
+ for (ExpenseList list : expiredLists) {
+ list.setInviteCode(null);
+ list.setInviteCodeExpiration(null);
+ expenseListRepository.save(list);
+ }
+ }
+}
diff --git a/src/main/java/de/zendric/app/xpensely_server/services/ExpenseListService.java b/src/main/java/de/zendric/app/xpensely_server/services/ExpenseListService.java
new file mode 100644
index 0000000..445ab1f
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/services/ExpenseListService.java
@@ -0,0 +1,184 @@
+package de.zendric.app.xpensely_server.services;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.zendric.app.xpensely_server.model.AppUser;
+import de.zendric.app.xpensely_server.model.Expense;
+import de.zendric.app.xpensely_server.model.ExpenseList;
+import de.zendric.app.xpensely_server.repo.ExpenseListRepository;
+import de.zendric.app.xpensely_server.repo.ExpenseRepository;
+import jakarta.persistence.EntityManager;
+
+@Service
+@Transactional
+public class ExpenseListService {
+
+ private ExpenseListRepository repository;
+ private final ExpenseRepository expenseRepository;
+
+ @Autowired
+ private EntityManager entityManager;
+
+ @Autowired
+ public ExpenseListService(ExpenseListRepository repository, ExpenseRepository expenseRepository) {
+ this.repository = repository;
+ this.expenseRepository = expenseRepository;
+ }
+
+ public List getAllLists() {
+ return repository.findAll();
+ }
+
+ public ExpenseList createList(ExpenseList list) {
+ return repository.save(list);
+ }
+
+ public void deleteList(Long id) {
+ repository.deleteById(id);
+ }
+
+ public void deleteById(Long id) {
+ repository.deleteById(id);
+ }
+
+ public Optional findById(Long id) {
+ return repository.findById(id);
+ }
+
+ public Iterable findAll() {
+ return repository.findAll();
+ }
+
+ public ExpenseList save(ExpenseList expenseList) {
+ return repository.save(expenseList);
+ }
+
+ public List findByUserId(Long id) {
+ List allLists = repository.findAll();
+ List userSpecificList = new ArrayList<>();
+ for (ExpenseList expenseList : allLists) {
+ AppUser sharedWith = expenseList.getSharedWith();
+
+ if (expenseList.getOwner().getId().equals(id)) {
+ userSpecificList.add(expenseList);
+ } else {
+ if (sharedWith != null && sharedWith.getId().equals(id)) {
+ userSpecificList.add(expenseList);
+ }
+ }
+ }
+ return userSpecificList;
+ }
+
+ public List findByUsername(String username) {
+ List allLists = repository.findAll();
+ List userSpecificList = new ArrayList<>();
+ for (ExpenseList expenseList : allLists) {
+ AppUser sharedWith = expenseList.getSharedWith();
+
+ if (expenseList.getOwner().getUsername().equals(username)) {
+ userSpecificList.add(expenseList);
+ } else {
+ if (sharedWith != null && sharedWith.getUsername().equals(username)) {
+ userSpecificList.add(expenseList);
+ }
+ }
+ }
+ return userSpecificList;
+
+ }
+
+ public Expense addExpenseToList(Long expenseListId, Expense expense) {
+ // find expenseList
+ ExpenseList expenseList = repository.findById(expenseListId)
+ .orElseThrow(() -> new RuntimeException("ExpenseList not found with id: " + expenseListId));
+ // get all added expenses
+ HashSet existingId = new HashSet<>();
+ for (Expense e : expenseList.getExpenses()) {
+ existingId.add(e.getId());
+ }
+ // add the new expense
+ expenseList.addExpense(expense);
+ // save
+ repository.save(expenseList);
+
+ Expense newExpense = new Expense();
+ for (Expense e : expenseList.getExpenses()) {
+ if (!existingId.contains(e.getId())) {
+ newExpense = e;
+ break;
+ }
+ }
+ return newExpense;
+ }
+
+ public void deleteExpenseFromList(Long expenseListId, Long expenseId) {
+ ExpenseList expenseList = repository.findById(expenseListId)
+ .orElseThrow(() -> new RuntimeException("ExpenseList not found with id: " + expenseListId));
+ Expense expenseToRemove = null;
+ for (Expense expense : expenseList.getExpenses()) {
+ if (expense.getId().equals(expenseId)) {
+ expenseToRemove = expense;
+ break;
+ }
+ }
+ if (expenseToRemove != null) {
+ expenseList.removeExpense(expenseToRemove);
+ } else {
+ throw new RuntimeException("Expense not found with id: " + expenseId);
+ }
+ repository.save(expenseList);
+ }
+
+ public String generateInviteCode(Long listId) {
+ ExpenseList list = repository.findById(listId)
+ .orElseThrow(() -> new RuntimeException("List not found"));
+ String inviteCode;
+ if (list.getInviteCode() == null || list.getInviteCodeExpiration().isBefore(LocalDateTime.now())) {
+
+ inviteCode = UUID.randomUUID().toString().substring(0, 6).toUpperCase();
+ LocalDateTime expirationTime = LocalDateTime.now().plusWeeks(1);
+
+ list.setInviteCode(inviteCode);
+ list.setInviteCodeExpiration(expirationTime);
+ repository.save(list);
+ } else {
+ inviteCode = list.getInviteCode();
+ }
+ return inviteCode;
+ }
+
+ public ExpenseList findByInviteCode(String inviteCode) {
+ return repository.findByInviteCode(inviteCode);
+ }
+
+ public Expense updateExpense(Long expenseListId, Expense updatedExpense) {
+ ExpenseList expenseList = repository.findById(expenseListId)
+ .orElseThrow(() -> new IllegalArgumentException("ExpenseList not found"));
+
+ if (!expenseList.getExpenses().stream()
+ .anyMatch(expense -> expense.getId().equals(updatedExpense.getId()))) {
+ throw new IllegalArgumentException("Expense does not belong to the specified ExpenseList");
+ }
+
+ Expense existingExpense = expenseRepository.findById(updatedExpense.getId())
+ .orElseThrow(() -> new IllegalArgumentException("Expense not found"));
+ existingExpense.setTitle(updatedExpense.getTitle());
+ existingExpense.setAmount(updatedExpense.getAmount());
+ existingExpense.setPersonalUseAmount(updatedExpense.getPersonalUseAmount());
+ existingExpense.setOtherPersonAmount(updatedExpense.getOtherPersonAmount());
+ existingExpense.setDate(updatedExpense.getDate());
+ existingExpense.setOwner(updatedExpense.getOwner());
+
+ return expenseRepository.save(existingExpense);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/zendric/app/xpensely_server/services/ExpenseService.java b/src/main/java/de/zendric/app/xpensely_server/services/ExpenseService.java
new file mode 100644
index 0000000..3d45353
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/services/ExpenseService.java
@@ -0,0 +1,19 @@
+package de.zendric.app.xpensely_server.services;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.zendric.app.xpensely_server.repo.ExpenseRepository;
+
+@Service
+@Transactional
+public class ExpenseService {
+
+ @Autowired
+ private ExpenseRepository repository;
+
+ public ExpenseService(ExpenseRepository repository) {
+ this.repository = repository;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/zendric/app/xpensely_server/services/UserService.java b/src/main/java/de/zendric/app/xpensely_server/services/UserService.java
new file mode 100644
index 0000000..58298dc
--- /dev/null
+++ b/src/main/java/de/zendric/app/xpensely_server/services/UserService.java
@@ -0,0 +1,64 @@
+package de.zendric.app.xpensely_server.services;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.stereotype.Service;
+
+import de.zendric.app.xpensely_server.model.AppUser;
+import de.zendric.app.xpensely_server.model.UsernameAlreadyExistsException;
+import de.zendric.app.xpensely_server.repo.UserRepository;
+
+@Service
+public class UserService {
+ private final UserRepository userRepository;
+
+ public UserService(UserRepository userRepository) {
+ this.userRepository = userRepository;
+ }
+
+ public List getAllUsers() {
+ return userRepository.findAll();
+ }
+
+ public AppUser createUser(AppUser user) {
+ if (Boolean.TRUE.equals(userRepository.existsByUsername(user.getUsername()))) {
+ throw new UsernameAlreadyExistsException("Username already exists");
+ }
+ return userRepository.save(user);
+ }
+
+ public AppUser getUser(Long id) {
+ Optional user = userRepository.findById(id);
+ if (user.isPresent()) {
+ return user.get();
+ } else
+ return null;
+ }
+
+ public AppUser deleteUserById(Long id) {
+ Optional user = userRepository.findById(id);
+ if (user.isPresent()) {
+ userRepository.deleteById(id);
+ return user.get();
+ } else
+ return null;
+ }
+
+ public AppUser getUserByName(String username) {
+ Optional optUser = userRepository.findByUsername(username);
+ if (optUser.isPresent()) {
+ return optUser.get();
+ } else
+ return null;
+ }
+
+ public AppUser getUserByGoogleId(String id) {
+ Optional optUser = userRepository.findByGoogleId(id);
+ if (optUser.isPresent()) {
+ return optUser.get();
+ } else
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index bc19518..61116e5 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,18 @@
+#Server
spring.application.name=XpenselyServer
+
+#Security
+spring.security.enabled=false
+spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
+spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
+spring.security.oauth2.resourceserver.jwt.issuer-uri=https://accounts.google.com
+
+# PostgreSQL Configuration
+spring.datasource.url=jdbc:postgresql://postgresdb:${DB_PORT}/${DB_P_NAME}
+spring.datasource.username=${DB_USERNAME}
+spring.datasource.password=${DB_PASSWORD}
+spring.datasource.driver-class-name=org.postgresql.Driver
+
+# Hibernate configuration
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
diff --git a/src/main/resources/static/xpensely_icon.png b/src/main/resources/static/xpensely_icon.png
new file mode 100644
index 0000000..b90a600
Binary files /dev/null and b/src/main/resources/static/xpensely_icon.png differ
diff --git a/src/main/resources/static/xpensely_logo.png b/src/main/resources/static/xpensely_logo.png
new file mode 100644
index 0000000..a77322b
Binary files /dev/null and b/src/main/resources/static/xpensely_logo.png differ
diff --git a/src/test/java/de/zendric/app/XpenselyServer/XpenselyServerApplicationTests.java b/src/test/java/de/zendric/app/xpensely_Server/XpenselyServerApplicationTests.java
similarity index 82%
rename from src/test/java/de/zendric/app/XpenselyServer/XpenselyServerApplicationTests.java
rename to src/test/java/de/zendric/app/xpensely_Server/XpenselyServerApplicationTests.java
index 5f228b2..3c78751 100644
--- a/src/test/java/de/zendric/app/XpenselyServer/XpenselyServerApplicationTests.java
+++ b/src/test/java/de/zendric/app/xpensely_Server/XpenselyServerApplicationTests.java
@@ -1,4 +1,4 @@
-package de.zendric.app.XpenselyServer;
+package de.zendric.app.xpensely_Server;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;