Compare commits
1 commit
main
...
recurring-
Author | SHA1 | Date | |
---|---|---|---|
f9a1521d65 |
10 changed files with 625 additions and 16 deletions
|
@ -2,7 +2,6 @@ package com.wbrawner.budgetserver.budget;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.permission.UserPermissionRequest;
|
import com.wbrawner.budgetserver.permission.UserPermissionRequest;
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -23,7 +22,6 @@ public class BudgetRequest {
|
||||||
this.users.addAll(users);
|
this.users.addAll(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public Set<UserPermissionRequest> getUsers() {
|
public Set<UserPermissionRequest> getUsers() {
|
||||||
return Set.copyOf(users);
|
return Set.copyOf(users);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,184 @@
|
||||||
|
package com.wbrawner.budgetserver.recurrence;
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.budget.Budget;
|
||||||
|
import com.wbrawner.budgetserver.category.Category;
|
||||||
|
import com.wbrawner.budgetserver.user.User;
|
||||||
|
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.JoinColumn;
|
||||||
|
import javax.persistence.ManyToOne;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import static com.wbrawner.budgetserver.Utils.randomId;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
public class RecurringTransaction {
|
||||||
|
@Id
|
||||||
|
private final String id = randomId();
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(nullable = false)
|
||||||
|
private final User createdBy;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private Long amount;
|
||||||
|
@ManyToOne
|
||||||
|
private Category category;
|
||||||
|
private Boolean expense;
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(nullable = false)
|
||||||
|
private Budget budget;
|
||||||
|
private String timeZone;
|
||||||
|
private int time;
|
||||||
|
private FrequencyUnit frequencyUnit;
|
||||||
|
private int frequencyValue;
|
||||||
|
|
||||||
|
public RecurringTransaction() {
|
||||||
|
this(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
FrequencyUnit.DAILY,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecurringTransaction(
|
||||||
|
String title,
|
||||||
|
String description,
|
||||||
|
FrequencyUnit frequencyUnit,
|
||||||
|
int frequencyValue,
|
||||||
|
String timeZone,
|
||||||
|
int time,
|
||||||
|
Long amount,
|
||||||
|
Category category,
|
||||||
|
Boolean expense,
|
||||||
|
User createdBy,
|
||||||
|
Budget budget
|
||||||
|
) {
|
||||||
|
this.title = title;
|
||||||
|
this.description = description;
|
||||||
|
this.frequencyUnit = frequencyUnit;
|
||||||
|
this.frequencyValue = frequencyValue;
|
||||||
|
setTimeZone(timeZone);
|
||||||
|
this.time = time;
|
||||||
|
this.amount = amount;
|
||||||
|
this.category = category;
|
||||||
|
this.expense = expense;
|
||||||
|
this.createdBy = createdBy;
|
||||||
|
this.budget = budget;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FrequencyUnit getFrequencyUnit() {
|
||||||
|
return frequencyUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFrequencyValue() {
|
||||||
|
return frequencyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFrequency(FrequencyUnit frequencyUnit, int frequencyValue) {
|
||||||
|
if (frequencyValue < 0) throw new IllegalArgumentException("frequencyValue must be at least 0");
|
||||||
|
if (frequencyValue > frequencyUnit.maxValue) {
|
||||||
|
throw new IllegalArgumentException(String.format(
|
||||||
|
"Invalid frequencyValue. Requested %d for %s but maxValue is %d",
|
||||||
|
frequencyValue,
|
||||||
|
frequencyUnit.name(),
|
||||||
|
frequencyUnit.maxValue
|
||||||
|
));
|
||||||
|
}
|
||||||
|
this.frequencyUnit = frequencyUnit;
|
||||||
|
this.frequencyValue = frequencyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTimeZone() {
|
||||||
|
return timeZone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeZone(String timeZone) {
|
||||||
|
this.timeZone = TimeZone.getTimeZone(timeZone).getID();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTimeOfDayInSeconds() {
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimeOfDayInSeconds(int time) {
|
||||||
|
this.time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAmount(Long amount) {
|
||||||
|
this.amount = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Category getCategory() {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCategory(Category category) {
|
||||||
|
this.category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean isExpense() {
|
||||||
|
return expense;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpense(Boolean expense) {
|
||||||
|
this.expense = expense;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getCreatedBy() {
|
||||||
|
return createdBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Budget getBudget() {
|
||||||
|
return budget;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBudget(Budget budget) {
|
||||||
|
this.budget = budget;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FrequencyUnit {
|
||||||
|
DAILY(0),
|
||||||
|
WEEKLY(7),
|
||||||
|
MONTHLY(30),
|
||||||
|
YEARLY(365);
|
||||||
|
|
||||||
|
int maxValue;
|
||||||
|
|
||||||
|
FrequencyUnit(int maxValue) {
|
||||||
|
this.maxValue = maxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
package com.wbrawner.budgetserver.recurrence;
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.ErrorResponse;
|
||||||
|
import com.wbrawner.budgetserver.category.Category;
|
||||||
|
import com.wbrawner.budgetserver.category.CategoryRepository;
|
||||||
|
import com.wbrawner.budgetserver.permission.Permission;
|
||||||
|
import com.wbrawner.budgetserver.permission.UserPermission;
|
||||||
|
import com.wbrawner.budgetserver.permission.UserPermissionRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static com.wbrawner.budgetserver.Utils.getCurrentUser;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping(path = "/recurrence")
|
||||||
|
@Transactional
|
||||||
|
public class RecurringTransactionController {
|
||||||
|
private final CategoryRepository categoryRepository;
|
||||||
|
private final RecurringTransactionRepository recurringTransactionRepository;
|
||||||
|
private final UserPermissionRepository userPermissionsRepository;
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(RecurringTransactionController.class);
|
||||||
|
|
||||||
|
public RecurringTransactionController(
|
||||||
|
CategoryRepository categoryRepository,
|
||||||
|
RecurringTransactionRepository recurringTransactionRepository,
|
||||||
|
UserPermissionRepository userPermissionsRepository
|
||||||
|
) {
|
||||||
|
this.categoryRepository = categoryRepository;
|
||||||
|
this.recurringTransactionRepository = recurringTransactionRepository;
|
||||||
|
this.userPermissionsRepository = userPermissionsRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
|
public ResponseEntity<List<RecurringTransactionResponse>> getRecurringTransactions(
|
||||||
|
@RequestParam("budgetId") String budgetId
|
||||||
|
) {
|
||||||
|
List<UserPermission> userPermissions = userPermissionsRepository.findAllByUserAndBudget_IdIn(
|
||||||
|
getCurrentUser(),
|
||||||
|
Collections.singletonList(budgetId),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
if (userPermissions.isEmpty()) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
var budget = userPermissions.get(0).getBudget();
|
||||||
|
var transactions = recurringTransactionRepository.findAllByBudget(budget)
|
||||||
|
.stream()
|
||||||
|
.map(RecurringTransactionResponse::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return ResponseEntity.ok(transactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(path = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
|
public ResponseEntity<RecurringTransactionResponse> getRecurringTransaction(@PathVariable String id) {
|
||||||
|
var budgets = userPermissionsRepository.findAllByUser(getCurrentUser(), null)
|
||||||
|
.stream()
|
||||||
|
.map(UserPermission::getBudget)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
var transaction = recurringTransactionRepository.findByIdAndBudgetIn(id, budgets).orElse(null);
|
||||||
|
if (transaction == null) return ResponseEntity.notFound().build();
|
||||||
|
return ResponseEntity.ok(new RecurringTransactionResponse(transaction));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
|
public ResponseEntity<Object> newTransaction(@RequestBody RecurringTransactionRequest request) {
|
||||||
|
var userResponse = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId())
|
||||||
|
.orElse(null);
|
||||||
|
if (userResponse == null) {
|
||||||
|
return ResponseEntity.badRequest().body(new ErrorResponse("Invalid budget ID"));
|
||||||
|
}
|
||||||
|
if (userResponse.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
var budget = userResponse.getBudget();
|
||||||
|
Category category = null;
|
||||||
|
if (request.getCategoryId() != null) {
|
||||||
|
category = categoryRepository.findByBudgetAndId(budget, request.getCategoryId()).orElse(null);
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(new RecurringTransactionResponse(recurringTransactionRepository.save(new RecurringTransaction(
|
||||||
|
request.getTitle(),
|
||||||
|
request.getDescription(),
|
||||||
|
request.getFrequencyUnit(),
|
||||||
|
request.getFrequencyValue(),
|
||||||
|
request.getTimeZone(),
|
||||||
|
request.getTime(),
|
||||||
|
request.getAmount(),
|
||||||
|
category,
|
||||||
|
request.getExpense(),
|
||||||
|
getCurrentUser(),
|
||||||
|
budget
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping(path = "/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||||
|
public ResponseEntity<Object> updateTransaction(@PathVariable String id, @RequestBody RecurringTransactionRequest request) {
|
||||||
|
var transaction = recurringTransactionRepository.findById(id).orElse(null);
|
||||||
|
if (transaction == null) return ResponseEntity.notFound().build();
|
||||||
|
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), transaction.getBudget().getId()).orElse(null);
|
||||||
|
if (userPermission == null) return ResponseEntity.notFound().build();
|
||||||
|
if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
if (request.getTitle() != null) {
|
||||||
|
transaction.setTitle(request.getTitle());
|
||||||
|
}
|
||||||
|
if (request.getDescription() != null) {
|
||||||
|
transaction.setDescription(request.getDescription());
|
||||||
|
}
|
||||||
|
if (request.getTimeZone() != null) {
|
||||||
|
transaction.setTimeZone(request.getTimeZone());
|
||||||
|
}
|
||||||
|
if (request.getAmount() != null) {
|
||||||
|
transaction.setAmount(request.getAmount());
|
||||||
|
}
|
||||||
|
if (request.getExpense() != null) {
|
||||||
|
transaction.setExpense(request.getExpense());
|
||||||
|
}
|
||||||
|
if (request.getTime() != null) {
|
||||||
|
transaction.setTimeOfDayInSeconds(request.getTime());
|
||||||
|
}
|
||||||
|
if (request.getFrequencyUnit() != null && request.getFrequencyValue() != null) {
|
||||||
|
transaction.setFrequency(request.getFrequencyUnit(), request.getFrequencyValue());
|
||||||
|
}
|
||||||
|
if (request.getBudgetId() != null) {
|
||||||
|
var newUserPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), request.getBudgetId()).orElse(null);
|
||||||
|
if (newUserPermission == null || newUserPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||||
|
return ResponseEntity
|
||||||
|
.badRequest()
|
||||||
|
.body(new ErrorResponse("Invalid budget"));
|
||||||
|
}
|
||||||
|
transaction.setBudget(newUserPermission.getBudget());
|
||||||
|
}
|
||||||
|
if (request.getCategoryId() != null) {
|
||||||
|
var category = categoryRepository.findByBudgetAndId(transaction.getBudget(), request.getCategoryId()).orElse(null);
|
||||||
|
if (category == null) {
|
||||||
|
return ResponseEntity
|
||||||
|
.badRequest()
|
||||||
|
.body(new ErrorResponse("Invalid category"));
|
||||||
|
}
|
||||||
|
transaction.setCategory(category);
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(new RecurringTransactionResponse(recurringTransactionRepository.save(transaction)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping(path = "/{id}", produces = {MediaType.TEXT_PLAIN_VALUE})
|
||||||
|
public ResponseEntity<Void> deleteTransaction(@PathVariable String id) {
|
||||||
|
var transaction = recurringTransactionRepository.findById(id).orElse(null);
|
||||||
|
if (transaction == null) return ResponseEntity.notFound().build();
|
||||||
|
// Check that the transaction belongs to an budget that the user has access to before deleting it
|
||||||
|
var userPermission = userPermissionsRepository.findByUserAndBudget_Id(getCurrentUser(), transaction.getBudget().getId()).orElse(null);
|
||||||
|
if (userPermission == null) return ResponseEntity.notFound().build();
|
||||||
|
if (userPermission.getPermission().isNotAtLeast(Permission.WRITE)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
recurringTransactionRepository.delete(transaction);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.wbrawner.budgetserver.recurrence;
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.budget.Budget;
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface RecurringTransactionRepository extends PagingAndSortingRepository<RecurringTransaction, String> {
|
||||||
|
Optional<RecurringTransaction> findByIdAndBudgetIn(String id, List<Budget> budgets);
|
||||||
|
|
||||||
|
List<RecurringTransaction> findAllByBudget(Budget budget);
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
package com.wbrawner.budgetserver.recurrence;
|
||||||
|
|
||||||
|
class RecurringTransactionRequest {
|
||||||
|
private final String title;
|
||||||
|
private final String description;
|
||||||
|
private final Long amount;
|
||||||
|
private final String categoryId;
|
||||||
|
private final Boolean expense;
|
||||||
|
private final String budgetId;
|
||||||
|
private final String timeZone;
|
||||||
|
private final Integer time;
|
||||||
|
private final RecurringTransaction.FrequencyUnit frequencyUnit;
|
||||||
|
private final Integer frequencyValue;
|
||||||
|
|
||||||
|
|
||||||
|
RecurringTransactionRequest() {
|
||||||
|
this(null, null, null, 0, null, 0, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecurringTransactionRequest(
|
||||||
|
String title,
|
||||||
|
String description,
|
||||||
|
RecurringTransaction.FrequencyUnit frequencyUnit,
|
||||||
|
int frequencyValue,
|
||||||
|
String timeZone,
|
||||||
|
int time,
|
||||||
|
Long amount,
|
||||||
|
String categoryId,
|
||||||
|
Boolean expense,
|
||||||
|
String budgetId
|
||||||
|
) {
|
||||||
|
this.title = title;
|
||||||
|
this.description = description;
|
||||||
|
this.frequencyUnit = frequencyUnit;
|
||||||
|
this.frequencyValue = frequencyValue;
|
||||||
|
this.timeZone = timeZone;
|
||||||
|
this.time = time;
|
||||||
|
this.amount = amount;
|
||||||
|
this.categoryId = categoryId;
|
||||||
|
this.expense = expense;
|
||||||
|
this.budgetId = budgetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTimeZone() {
|
||||||
|
return timeZone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTime() {
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecurringTransaction.FrequencyUnit getFrequencyUnit() {
|
||||||
|
return frequencyUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getFrequencyValue() {
|
||||||
|
return frequencyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getAmount() {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCategoryId() {
|
||||||
|
return categoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getExpense() {
|
||||||
|
return expense;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBudgetId() {
|
||||||
|
return budgetId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.wbrawner.budgetserver.recurrence;
|
||||||
|
|
||||||
|
class RecurringTransactionResponse {
|
||||||
|
public final String id;
|
||||||
|
public final String title;
|
||||||
|
public final String description;
|
||||||
|
public final long amount;
|
||||||
|
public final boolean expense;
|
||||||
|
public final String budgetId;
|
||||||
|
public final String categoryId;
|
||||||
|
public final String createdBy;
|
||||||
|
public final String timeZone;
|
||||||
|
public final int time;
|
||||||
|
public final RecurringTransaction.FrequencyUnit frequencyUnit;
|
||||||
|
public final int frequencyValue;
|
||||||
|
|
||||||
|
|
||||||
|
RecurringTransactionResponse(
|
||||||
|
String id,
|
||||||
|
String title,
|
||||||
|
String description,
|
||||||
|
long amount,
|
||||||
|
boolean expense,
|
||||||
|
String budgetId,
|
||||||
|
String categoryId,
|
||||||
|
String createdBy,
|
||||||
|
String timeZone,
|
||||||
|
int time,
|
||||||
|
RecurringTransaction.FrequencyUnit frequencyUnit,
|
||||||
|
int frequencyValue
|
||||||
|
) {
|
||||||
|
this.id = id;
|
||||||
|
this.title = title;
|
||||||
|
this.description = description;
|
||||||
|
this.amount = amount;
|
||||||
|
this.expense = expense;
|
||||||
|
this.budgetId = budgetId;
|
||||||
|
this.categoryId = categoryId;
|
||||||
|
this.createdBy = createdBy;
|
||||||
|
this.timeZone = timeZone;
|
||||||
|
this.time = time;
|
||||||
|
this.frequencyUnit = frequencyUnit;
|
||||||
|
this.frequencyValue = frequencyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RecurringTransactionResponse(RecurringTransaction recurringTransaction) {
|
||||||
|
this(
|
||||||
|
recurringTransaction.getId(),
|
||||||
|
recurringTransaction.getTitle(),
|
||||||
|
recurringTransaction.getDescription(),
|
||||||
|
recurringTransaction.getAmount(),
|
||||||
|
recurringTransaction.isExpense(),
|
||||||
|
recurringTransaction.getBudget().getId(),
|
||||||
|
recurringTransaction.getCategory() != null ? recurringTransaction.getCategory().getId() : null,
|
||||||
|
recurringTransaction.getCreatedBy().getId(),
|
||||||
|
recurringTransaction.getTimeZone(),
|
||||||
|
recurringTransaction.getTimeOfDayInSeconds(),
|
||||||
|
recurringTransaction.getFrequencyUnit(),
|
||||||
|
recurringTransaction.getFrequencyValue()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package com.wbrawner.budgetserver.recurrence;
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.transaction.Transaction;
|
||||||
|
import com.wbrawner.budgetserver.transaction.TransactionRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class RecurringTransactionTask {
|
||||||
|
private final RecurringTransactionRepository recurringTransactionRepository;
|
||||||
|
private final TransactionRepository transactionRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public RecurringTransactionTask(
|
||||||
|
RecurringTransactionRepository recurringTransactionRepository,
|
||||||
|
TransactionRepository transactionRepository
|
||||||
|
) {
|
||||||
|
this.recurringTransactionRepository = recurringTransactionRepository;
|
||||||
|
this.transactionRepository = transactionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(cron = "0 0 * * * *")
|
||||||
|
public void createTransactions() {
|
||||||
|
recurringTransactionRepository.findAll().forEach(recurringTransaction -> {
|
||||||
|
GregorianCalendar today = new GregorianCalendar(TimeZone.getTimeZone(recurringTransaction.getTimeZone()));
|
||||||
|
// if recurrence matches today, create a new transaction
|
||||||
|
int adjustedFrequencyValue, calendarField;
|
||||||
|
switch (recurringTransaction.getFrequencyUnit()) {
|
||||||
|
case DAILY -> {
|
||||||
|
// Daily transactions should have the frequency value set to 0, so we just force it to be the same
|
||||||
|
// as the current date in order to force the transaction creation.
|
||||||
|
adjustedFrequencyValue = today.get(Calendar.DATE);
|
||||||
|
calendarField = Calendar.DATE;
|
||||||
|
}
|
||||||
|
case WEEKLY -> {
|
||||||
|
// No adjustments needed for day of week
|
||||||
|
adjustedFrequencyValue = today.get(Calendar.DAY_OF_WEEK);
|
||||||
|
calendarField = Calendar.DAY_OF_WEEK;
|
||||||
|
}
|
||||||
|
case MONTHLY -> {
|
||||||
|
// Check if the day of the month is correct
|
||||||
|
adjustedFrequencyValue = Math.min(recurringTransaction.getFrequencyValue(), today.getActualMaximum(Calendar.DAY_OF_MONTH));
|
||||||
|
calendarField = Calendar.DAY_OF_MONTH;
|
||||||
|
}
|
||||||
|
case YEARLY -> {
|
||||||
|
adjustedFrequencyValue = recurringTransaction.getFrequencyValue();
|
||||||
|
if (today.isLeapYear(today.get(Calendar.YEAR)) && today.get(Calendar.DAY_OF_YEAR) >= 31 + 29) {
|
||||||
|
// We're just pretending that Feb 29th doesn't exist here...
|
||||||
|
adjustedFrequencyValue -= 1;
|
||||||
|
}
|
||||||
|
calendarField = Calendar.DAY_OF_YEAR;
|
||||||
|
}
|
||||||
|
default -> throw new IllegalStateException("Unexpected value: " + recurringTransaction.getFrequencyUnit());
|
||||||
|
}
|
||||||
|
if (adjustedFrequencyValue == today.get(calendarField)) {
|
||||||
|
createTransaction(recurringTransaction, today);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTransaction(RecurringTransaction recurringTransaction, Calendar transactionCalendar) {
|
||||||
|
transactionCalendar.set(Calendar.HOUR, 0);
|
||||||
|
transactionCalendar.set(Calendar.MINUTE, 0);
|
||||||
|
transactionCalendar.set(Calendar.SECOND, 0);
|
||||||
|
transactionCalendar.set(Calendar.MILLISECOND, 0);
|
||||||
|
transactionCalendar.add(Calendar.SECOND, recurringTransaction.getTimeOfDayInSeconds());
|
||||||
|
transactionRepository.save(new Transaction(
|
||||||
|
recurringTransaction.getTitle(),
|
||||||
|
recurringTransaction.getDescription(),
|
||||||
|
transactionCalendar.toInstant(),
|
||||||
|
recurringTransaction.getAmount(),
|
||||||
|
recurringTransaction.getCategory(),
|
||||||
|
recurringTransaction.isExpense(),
|
||||||
|
recurringTransaction.getCreatedBy(),
|
||||||
|
recurringTransaction.getBudget(),
|
||||||
|
recurringTransaction
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,13 @@ package com.wbrawner.budgetserver.transaction;
|
||||||
|
|
||||||
import com.wbrawner.budgetserver.budget.Budget;
|
import com.wbrawner.budgetserver.budget.Budget;
|
||||||
import com.wbrawner.budgetserver.category.Category;
|
import com.wbrawner.budgetserver.category.Category;
|
||||||
|
import com.wbrawner.budgetserver.recurrence.RecurringTransaction;
|
||||||
import com.wbrawner.budgetserver.user.User;
|
import com.wbrawner.budgetserver.user.User;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.JoinColumn;
|
||||||
|
import javax.persistence.ManyToOne;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
import static com.wbrawner.budgetserver.Utils.randomId;
|
import static com.wbrawner.budgetserver.Utils.randomId;
|
||||||
|
@ -26,19 +30,24 @@ public class Transaction implements Comparable<Transaction> {
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(nullable = false)
|
@JoinColumn(nullable = false)
|
||||||
private Budget budget;
|
private Budget budget;
|
||||||
|
@ManyToOne
|
||||||
|
private RecurringTransaction recurrence;
|
||||||
|
|
||||||
public Transaction() {
|
public Transaction() {
|
||||||
this(null, null, null, null, null, null, null, null);
|
this(null, null, null, null, null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Transaction(String title,
|
public Transaction(
|
||||||
String description,
|
String title,
|
||||||
Instant date,
|
String description,
|
||||||
Long amount,
|
Instant date,
|
||||||
Category category,
|
Long amount,
|
||||||
Boolean expense,
|
Category category,
|
||||||
User createdBy,
|
Boolean expense,
|
||||||
Budget budget) {
|
User createdBy,
|
||||||
|
Budget budget,
|
||||||
|
RecurringTransaction recurrence
|
||||||
|
) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
|
@ -47,11 +56,10 @@ public class Transaction implements Comparable<Transaction> {
|
||||||
this.expense = expense;
|
this.expense = expense;
|
||||||
this.createdBy = createdBy;
|
this.createdBy = createdBy;
|
||||||
this.budget = budget;
|
this.budget = budget;
|
||||||
|
this.recurrence = recurrence;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
// This should only be set from Hibernate so it shouldn't actually be null ever
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,6 +123,14 @@ public class Transaction implements Comparable<Transaction> {
|
||||||
this.budget = budget;
|
this.budget = budget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RecurringTransaction getRecurrence() {
|
||||||
|
return this.recurrence;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecurrence(RecurringTransaction recurrence) {
|
||||||
|
this.recurrence = recurrence;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(Transaction other) {
|
public int compareTo(Transaction other) {
|
||||||
return this.date.compareTo(other.date);
|
return this.date.compareTo(other.date);
|
||||||
|
|
|
@ -144,7 +144,8 @@ public class TransactionController {
|
||||||
category,
|
category,
|
||||||
request.getExpense(),
|
request.getExpense(),
|
||||||
getCurrentUser(),
|
getCurrentUser(),
|
||||||
budget
|
budget,
|
||||||
|
null
|
||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.2.2.RELEASE"
|
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue