diff --git a/api/src/main/java/com/wbrawner/budgetserver/Utils.java b/api/src/main/java/com/wbrawner/budgetserver/Utils.java new file mode 100644 index 0000000..2bca0bd --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/Utils.java @@ -0,0 +1,23 @@ +package com.wbrawner.budgetserver; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +public final class Utils { + private static int[] CALENDAR_FIELDS = new int[]{ + Calendar.MILLISECOND, + Calendar.SECOND, + Calendar.MINUTE, + Calendar.HOUR_OF_DAY, + Calendar.DATE + }; + + public static Date getFirstOfMonth() { + GregorianCalendar calendar = new GregorianCalendar(); + for (int field : CALENDAR_FIELDS) { + calendar.set(field, calendar.getActualMinimum(field)); + } + return calendar.getTime(); + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/Budget.java b/api/src/main/java/com/wbrawner/budgetserver/budget/Budget.java new file mode 100644 index 0000000..3a8b826 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/budget/Budget.java @@ -0,0 +1,81 @@ +package com.wbrawner.budgetserver.budget; + +import com.wbrawner.budgetserver.category.Category; +import com.wbrawner.budgetserver.transaction.Transaction; + +import javax.persistence.*; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; + +@Entity +public class Budget { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + private String name; + private String description; + private String currencyCode; + @OneToMany(mappedBy = "budget") + private final Set transactions = new TreeSet<>(); + @OneToMany(mappedBy = "budget") + private final Set categories = new TreeSet<>(); + @OneToMany(mappedBy = "budget") + private final Set users = new HashSet<>(); + + public Budget() {} + + public Budget(String name, String description) { + this(name, description, "USD"); + } + + public Budget(String name, String description, String currencyCode) { + this.name = name; + this.description = description; + this.currencyCode = currencyCode; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCurrencyCode() { + return currencyCode; + } + + public void setCurrencyCode(String currencyCode) { + this.currencyCode = currencyCode; + } + + public Set getTransactions() { + return transactions; + } + + public Set getCategories() { + return categories; + } + + public Set getUsers() { + return users; + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/Budget.kt b/api/src/main/java/com/wbrawner/budgetserver/budget/Budget.kt deleted file mode 100644 index 74be025..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/budget/Budget.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.wbrawner.budgetserver.budget - -import com.wbrawner.budgetserver.category.Category -import com.wbrawner.budgetserver.permission.UserPermission -import com.wbrawner.budgetserver.permission.UserPermissionRequest -import com.wbrawner.budgetserver.permission.UserPermissionResponse -import com.wbrawner.budgetserver.transaction.Transaction -import java.util.* -import javax.persistence.* - -@Entity -data class Budget( - @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null, - val name: String = "", - val description: String? = null, - val currencyCode: String? = null, - @OneToMany(mappedBy = "budget") val transactions: MutableSet = TreeSet(), - @OneToMany(mappedBy = "budget") val categories: MutableSet = TreeSet(), - @OneToMany(mappedBy = "budget") val users: MutableSet = mutableSetOf() -) - -data class NewBudgetRequest(val name: String, val description: String?, val users: Set) - -data class UpdateBudgetRequest(val name: String?, val description: String?, val users: Set?) - -data class BudgetResponse(val id: Long, val name: String, val description: String?, val users: List) { - constructor(budget: Budget, users: List) : this( - budget.id!!, - budget.name, - budget.description, - users.map { UserPermissionResponse(it) } - ) -} - -data class BudgetBalanceResponse(val id: Long, val balance: Long) diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetBalanceResponse.java b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetBalanceResponse.java new file mode 100644 index 0000000..840f0af --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetBalanceResponse.java @@ -0,0 +1,11 @@ +package com.wbrawner.budgetserver.budget; + +public class BudgetBalanceResponse { + public final long id; + public final long balance; + + public BudgetBalanceResponse(long id, long balance) { + this.id = id; + this.balance = balance; + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.java b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.java new file mode 100644 index 0000000..8028fbb --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.java @@ -0,0 +1,202 @@ +package com.wbrawner.budgetserver.budget; + +import com.wbrawner.budgetserver.permission.Permission; +import com.wbrawner.budgetserver.permission.UserPermission; +import com.wbrawner.budgetserver.permission.UserPermissionRepository; +import com.wbrawner.budgetserver.transaction.TransactionRepository; +import com.wbrawner.budgetserver.user.User; +import com.wbrawner.budgetserver.user.UserRepository; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.Authorization; +import org.hibernate.Hibernate; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static com.wbrawner.budgetserver.Utils.getFirstOfMonth; +import static com.wbrawner.budgetserver.UtilsKt.getCurrentUser; + +@RestController +@RequestMapping(value = "/budgets") +@Api(value = "Budgets", tags = {"Budgets"}, authorizations = {@Authorization(value = "basic")}) +@Transactional +public class BudgetController { + private final BudgetRepository budgetRepository; + private final TransactionRepository transactionRepository; + private final UserRepository userRepository; + private final UserPermissionRepository userPermissionsRepository; + + public BudgetController( + BudgetRepository budgetRepository, + TransactionRepository transactionRepository, + UserRepository userRepository, + UserPermissionRepository userPermissionsRepository + ) { + this.budgetRepository = budgetRepository; + this.transactionRepository = transactionRepository; + this.userRepository = userRepository; + this.userPermissionsRepository = userPermissionsRepository; + } + + @GetMapping(value = "", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "getBudgets", nickname = "getBudgets", tags = {"Budgets"}) + public ResponseEntity> getBudgets(Integer page, Integer count) { + User user = getCurrentUser(); + if (user == null) { + return ResponseEntity.status(401).build(); + } + + List budgets = userPermissionsRepository.findAllByUser( + getCurrentUser(), + PageRequest.of( + page != null ? page : 0, + count != null ? count : 1000 + ) + ) + .stream() + .map(userPermission -> { + Budget budget = userPermission.getBudget(); + if (budget == null) { + return null; + } +// Hibernate.initialize(budget); + return new BudgetResponse(budget, userPermissionsRepository.findAllByBudget(budget, null)); + }) + .collect(Collectors.toList()); + + return ResponseEntity.ok(budgets); + } + + @GetMapping(value = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "getBudget", nickname = "getBudget", tags = {"Budgets"}) + public ResponseEntity getBudget(@PathVariable long id) { + var user = getCurrentUser(); + if (user == null) { + return ResponseEntity.status(401).build(); + } + + var userPermission = userPermissionsRepository.findAllByUserAndBudget_Id(user, id, null).get(0); + if (userPermission == null) { + return ResponseEntity.notFound().build(); + } + + var budget = userPermission.getBudget(); + if (budget == null) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(new BudgetResponse(budget, userPermissionsRepository.findAllByBudget(budget, null))); + } + + @GetMapping(value = "/{id}/balance", produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "getBudgetBalance", nickname = "getBudgetBalance", tags = {"Budgets"}) + public ResponseEntity getBudgetBalance(@PathVariable long id) { + var user = getCurrentUser(); + if (user == null) { + return ResponseEntity.status(401).build(); + } + + var userPermission = userPermissionsRepository.findAllByUserAndBudget_Id(user, id, null).get(0); + if (userPermission == null) { + return ResponseEntity.notFound().build(); + } + + var budget = userPermission.getBudget(); + if (budget == null) { + return ResponseEntity.notFound().build(); + } + var balance = transactionRepository.sumBalanceByBudgetId(budget.getId(), getFirstOfMonth()); + return ResponseEntity.ok(new BudgetBalanceResponse(budget.getId(), balance)); + } + + @PostMapping(value = "/new", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "newBudget", nickname = "newBudget", tags = {"Budgets"}) + public ResponseEntity newBudget(@RequestBody BudgetRequest request) { + final var budget = budgetRepository.save(new Budget(request.name, request.description)); + var users = request.getUsers() + .stream() + .map(userPermissionRequest -> { + var user = userRepository.findById(userPermissionRequest.getUser()).orElse(null); + if (user == null) { + return null; + } + + return userPermissionsRepository.save( + new UserPermission(budget, user, userPermissionRequest.getPermission()) + ); + }) + .collect(Collectors.toSet()); + + var currentUserIncluded = users.stream().anyMatch(userPermission -> + userPermission.getUser().getId().equals(getCurrentUser().getId()) + ); + if (!currentUserIncluded) { + users.add( + userPermissionsRepository.save( + new UserPermission(budget, getCurrentUser(), Permission.OWNER) + ) + ); + } + return ResponseEntity.ok(new BudgetResponse(budget, new ArrayList<>(users))); + } + + @PutMapping(value = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE}) + @ApiOperation(value = "updateBudget", nickname = "updateBudget", tags = {"Budgets"}) + public ResponseEntity updateBudget(@PathVariable long id, BudgetRequest request) { + var user = getCurrentUser(); + if (user == null) { + return ResponseEntity.status(401).build(); + } + + var userPermission = userPermissionsRepository.findAllByUserAndBudget_Id(user, id, null).get(0); + if (userPermission == null) { + return ResponseEntity.notFound().build(); + } + + var budget = userPermission.getBudget(); + if (budget == null) { + return ResponseEntity.notFound().build(); + } + + if (request.name != null) { + budget.setName(request.name); + } + + if (request.description != null) { + budget.setDescription(request.description); + } + + if (!request.getUsers().isEmpty()) { + request.getUsers().forEach(userPermissionRequest -> { + var requestedUser = userRepository.findById(userPermissionRequest.getUser()).orElse(null); + if (requestedUser != null) { + userPermissionsRepository.save(new UserPermission(budget, requestedUser, userPermissionRequest.getPermission())); + } + }); + } + userPermissionsRepository.findAllByUserAndBudget(getCurrentUser() !!, budget, null) + return ResponseEntity.ok(BudgetResponse(budgetRepository.save(budget), users)) + } + + @DeleteMapping(value = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE}) + @ApiOperation(value = "deleteBudget", nickname = "deleteBudget", tags = {"Budgets"}) + open fun + + deleteBudget(@PathVariable id:Long):ResponseEntity + + { + val budget = userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser() !!, id, null) + .firstOrNull() + ?.budget + ?:return ResponseEntity.notFound().build() + budgetRepository.delete(budget) + return ResponseEntity.ok().build() + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.kt b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.kt index f972074..fd5dd8b 100644 --- a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.kt +++ b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetController.kt @@ -20,7 +20,7 @@ import javax.transaction.Transactional @RequestMapping("/budgets") @Api(value = "Budgets", tags = ["Budgets"], authorizations = [Authorization("basic")]) @Transactional -open class BudgetController( +open class BudgetControllerKt( private val budgetRepository: BudgetRepository, private val transactionRepository: TransactionRepository, private val userRepository: UserRepository, @@ -60,8 +60,8 @@ open class BudgetController( @PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "newBudget", nickname = "newBudget", tags = ["Budgets"]) - open fun newBudget(@RequestBody request: NewBudgetRequest): ResponseEntity { - val budget = budgetRepository.save(Budget(name = request.name, description = request.description)) + open fun newBudget(@RequestBody request: BudgetRequest): ResponseEntity { + val budget = budgetRepository.save(Budget(request.name, request.description)) val users = request.users .mapNotNull { userRepository.findById(it.user).orElse(null)?.let { user -> @@ -78,21 +78,21 @@ open class BudgetController( ) ) } - return ResponseEntity.ok(BudgetResponse(budget, users.toList())) + return ResponseEntity.ok(BudgetResponse(budget, users.toMutableList())) } @PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE]) @ApiOperation(value = "updateBudget", nickname = "updateBudget", tags = ["Budgets"]) - open fun updateBudget(@PathVariable id: Long, request: UpdateBudgetRequest): ResponseEntity { + open fun updateBudget(@PathVariable id: Long, request: BudgetRequest): ResponseEntity { var budget = userPermissionsRepository.findAllByUserAndBudget_Id(getCurrentUser()!!, id, null) .firstOrNull() ?.budget ?: return ResponseEntity.notFound().build() request.name?.let { - budget = budget.copy(name = it) + budget.name = it } request.description?.let { - budget = budget.copy(description = request.description) + budget.description = it } val users = request.users?.mapNotNull { req -> userRepository.findById(req.user).orElse(null)?.let { diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRepository.java b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRepository.java new file mode 100644 index 0000000..7577379 --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRepository.java @@ -0,0 +1,6 @@ +package com.wbrawner.budgetserver.budget; + +import org.springframework.data.repository.PagingAndSortingRepository; + +public interface BudgetRepository extends PagingAndSortingRepository { +} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRepository.kt b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRepository.kt deleted file mode 100644 index de16ba0..0000000 --- a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRepository.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.wbrawner.budgetserver.budget - -import org.springframework.data.repository.PagingAndSortingRepository - -interface BudgetRepository: PagingAndSortingRepository { - fun findAllByIdIn(ids: List): List -// fun findByUsersContainsAndId(user: User, id: Long): Optional -// fun findByUsersContainsAndTransactionsContains(user: User, transaction: Transaction): Optional -// fun findByUsersContainsAndCategoriesContains(user: User, category: Category): Optional -} \ No newline at end of file diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRequest.java b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRequest.java new file mode 100644 index 0000000..5c95a9d --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetRequest.java @@ -0,0 +1,24 @@ +package com.wbrawner.budgetserver.budget; + +import com.wbrawner.budgetserver.permission.UserPermissionRequest; + +import javax.validation.constraints.NotNull; +import java.util.HashSet; +import java.util.Set; + +public class BudgetRequest { + public final String name; + public final String description; + private final Set users = new HashSet<>(); + + public BudgetRequest(String name, String description, Set users) { + this.name = name; + this.description = description; + this.users.addAll(users); + } + + @NotNull + public Set getUsers() { + return Set.copyOf(users); + } +} diff --git a/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetResponse.java b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetResponse.java new file mode 100644 index 0000000..1ac852e --- /dev/null +++ b/api/src/main/java/com/wbrawner/budgetserver/budget/BudgetResponse.java @@ -0,0 +1,38 @@ +package com.wbrawner.budgetserver.budget; + +import com.wbrawner.budgetserver.permission.UserPermission; +import com.wbrawner.budgetserver.permission.UserPermissionResponse; +import com.wbrawner.budgetserver.user.User; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class BudgetResponse { + public final long id; + public final String name; + public final String description; + private final List users; + + public BudgetResponse(long id, String name, String description, List users) { + this.id = id; + this.name = name; + this.description = description; + this.users = users; + } + + public BudgetResponse(Budget budget, List users) { + this( + Objects.requireNonNull(budget.getId()), + budget.getName(), + budget.getDescription(), + users.stream() + .map(UserPermissionResponse::new) + .collect(Collectors.toList()) + ); + } + + public List getUsers() { + return List.copyOf(users); + } +}