fix: major changes

This commit is contained in:
filippo-ferrari 2024-09-08 16:10:50 +02:00
parent 6475f83dd8
commit 50a0325341
7 changed files with 224 additions and 136 deletions

View file

@ -13,7 +13,7 @@ import java.time.LocalDateTime;
@Getter
@Setter
@Table(name = "expenses")
public class Expense extends AbstractEntity {
public class Expense {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ -46,11 +46,11 @@ public class Expense extends AbstractEntity {
@ManyToOne
@JoinColumn(name = "CreditorId")
private Person creditor;
private Person payer;
@ManyToOne
@JoinColumn(name = "DebtorId")
private Person debtor;
private Person beneficiary;
@ManyToOne
@JoinColumn(name = "EventId")

View file

@ -40,11 +40,12 @@ public class Person extends AbstractEntity {
@Column(name = "credit")
private BigDecimal credit;
@OneToMany(mappedBy = "creditor")
private Set<Expense> creditExpenses;
// Updated to match the new field names in Expense
@OneToMany(mappedBy = "payer")
private Set<Expense> expensesAsPayer;
@OneToMany(mappedBy = "debtor")
private Set<Expense> debtExpenses;
@OneToMany(mappedBy = "beneficiary")
private Set<Expense> expensesAsBeneficiary;
@ManyToMany(mappedBy = "participants")
private Set<Event> events;

View file

@ -14,24 +14,24 @@ import java.util.Set;
public interface ExpenseRepository extends JpaRepository<Expense, Long>, JpaSpecificationExecutor<Expense> {
// Find expenses where the creditor is a specific person
@Query("SELECT e FROM Expense e WHERE e.creditor.id = :personId")
Set<Expense> findCreditorsExpensesByPersonId(@Param("personId") Long personId);
@Query("SELECT e FROM Expense e WHERE e.payer.id = :personId")
Set<Expense> findExpensesByPayer(@Param("personId") Long personId);
// Find expenses where the debtor is a specific person
@Query("SELECT e FROM Expense e WHERE e.debtor.id = :personId")
Set<Expense> findDebtorsExpensesByPersonId(@Param("personId") Long personId);
@Query("SELECT e FROM Expense e WHERE e.beneficiary.id = :personId")
Set<Expense> findExpensesByBeneficiary(@Param("personId") Long personId);
// Find all expenses for a given year
@Query("SELECT e FROM Expense e WHERE YEAR(e.date) = :year")
List<Expense> findAllByYear(@Param("year") int year);
// Find unpaid expenses where the creditor is a specific person
@Query("SELECT e FROM Expense e WHERE e.creditor.id = :personId AND e.isPaid = false")
Set<Expense> findUnpaidCreditorsExpensesByPersonId(@Param("personId") Long personId);
@Query("SELECT e FROM Expense e WHERE e.payer.id = :personId AND e.isPaid = false")
Set<Expense> findUnpaidExpensesByPayer(@Param("personId") Long personId);
// Find unpaid expenses where the debtor is a specific person
@Query("SELECT e FROM Expense e WHERE e.debtor.id = :personId AND e.isPaid = false")
Set<Expense> findUnpaidDebtorsExpensesByPersonId(@Param("personId") Long personId);
@Query("SELECT e FROM Expense e WHERE e.beneficiary.id = :personId AND e.isPaid = false")
Set<Expense> findUnapidExpensesByBeneficiary(@Param("personId") Long personId);
// Find expenses for a given year and filter by expense type and paid status
@Query("SELECT e FROM Expense e WHERE YEAR(e.date) = :year AND NOT (e.expenseType = :expenseType AND e.isPaid = true)")

View file

@ -26,99 +26,102 @@ public class ExpenseService {
this.expenseRepository = expenseRepository;
}
/**
* Retrieves an expense by its ID.
* @param id the ID of the expense
* @return an Optional containing the expense if found, otherwise empty
*/
public Optional<Expense> get(Long id) {
return expenseRepository.findById(id);
}
/**
* finds all expenses tagged as debit given a user
* Finds all expenses where the specified person is the beneficiary.
* @param person the user of the expenses
* @return the collections of expenses found
* @return the collection of expenses found
*/
public Collection<Expense> findDebtByUser(final Person person) {
return expenseRepository.findDebtorsExpensesByPersonId(person.getId());
public Collection<Expense> findExpensesWhereBeneficiary(final Person person) {
return expenseRepository.findExpensesByBeneficiary(person.getId());
}
/**
* finds all expenses tagged as credit given a user
* Finds all expenses where the specified person is the payer.
* @param person the user of the expenses
* @return the collections of expenses found
* @return the collection of expenses found
*/
public Collection<Expense> findCreditByUser(final Person person) {
return expenseRepository.findCreditorsExpensesByPersonId(person.getId());
public Collection<Expense> findExpensesWherePayer(final Person person) {
return expenseRepository.findExpensesByPayer(person.getId());
}
/**
* finds all expenses tagged as debit and unpaid given a user
* Finds all expenses where the specified person is the beneficiary and the expense is unpaid.
* @param person the user of the expenses
* @return the collections of expenses found
* @return the collection of unpaid expenses found
*/
public Collection<Expense> findUnpaidDebtByUser(final Person person) {
return expenseRepository.findUnpaidDebtorsExpensesByPersonId(person.getId());
public Collection<Expense> findUnpaidExpensesWhereBeneficiary(final Person person) {
return expenseRepository.findUnapidExpensesByBeneficiary(person.getId());
}
/**
* finds all expenses tagged as credit and unpaid given a user
* Finds all expenses where the specified person is the payer and the expense is unpaid.
* @param person the user of the expenses
* @return the collections of expenses found
* @return the collection of unpaid expenses found
*/
public Collection<Expense> findUnpaidCreditByUser(final Person person) {
return expenseRepository.findUnpaidCreditorsExpensesByPersonId(person.getId());
public Collection<Expense> findUnpaidExpensesWherePayer(final Person person) {
return expenseRepository.findUnpaidExpensesByPayer(person.getId());
}
/**
* finds all expenses related to a user
* Finds all expenses related to a user, both where the user is a payer and a beneficiary.
* @param person the user of the expenses
* @return the collections of expenses found
* @return the list of expenses found
*/
public List<Expense> findExpenseByUser(final Person person) {
final var credits = this.findCreditByUser(person);
final var debits = this.findDebtByUser(person);
return Stream.concat(credits.stream(), debits.stream()).toList();
public List<Expense> findExpensesByUser(final Person person) {
// Retrieve expenses where the person is the payer
final var payerExpenses = this.findExpensesWherePayer(person);
// Retrieve expenses where the person is the beneficiary
final var beneficiaryExpenses = this.findExpensesWhereBeneficiary(person);
// Combine both sets of expenses into a single list without duplicates
return Stream.concat(payerExpenses.stream(), beneficiaryExpenses.stream())
.distinct()
.toList();
}
public List<Expense> findAll() {return expenseRepository.findAll();}
/**
* updates an expense
* @param entity the expense to update
* Retrieves all expenses.
* @return the list of all expenses
*/
public void update(Expense entity) {
if (Boolean.TRUE.equals(entity.getIsPaid())) entity.setPaymentDate(LocalDateTime.now());
this.setExpenseType(entity);
expenseRepository.save(entity);
public List<Expense> findAll() {
return expenseRepository.findAll();
}
/**
* deletes an expense given the ID
* @param id the id of the expense to delete
* Finds all expenses for a given year.
* @param year the year for which to find expenses
* @return the list of expenses found
*/
public void delete(Long id) {
expenseRepository.deleteById(id);
}
public Page<Expense> list(Pageable pageable) {
return expenseRepository.findAll(pageable);
}
public Page<Expense> list(Pageable pageable, Specification<Expense> filter) {
return expenseRepository.findAll(filter, pageable);
}
public int count() {
return (int) expenseRepository.count();
}
public List<Expense> findAllByYear(final int year ) {
public List<Expense> findAllByYear(final int year) {
return this.expenseRepository.findAllByYear(year);
}
/**
* Fetches all expenses ordered by date in descending order.
* @return the list of expenses found
*/
public List<Expense> findAllOrderByDateDescending() {
return this.expenseRepository.findAllByOrderByDateDesc();
}
/**
* Finds expenses by year excluding those marked as credit and paid.
* @param year the year for which to find expenses
* @return the list of expenses found
*/
public List<Expense> findExpensesByYearExcludingCreditPaid(int year) {
return expenseRepository.findByYearAndFilterCreditPaid(year, ExpenseType.CREDIT);
}
/**
* checks if an expense has been paid
* Checks if an expense has been paid.
* @param expense the expense to check
* @return true if the expense has been paid, false otherwise
*/
@ -127,35 +130,76 @@ public class ExpenseService {
}
/**
* fetches all expenses ordered by date descending
* @return the list of expenses found
* Updates an existing expense.
* @param entity the expense to update
*/
public List<Expense> findAllOrderByDateDescending() {
return this.expenseRepository.findAllByOrderByDateDesc();
public void update(Expense entity) {
if (Boolean.TRUE.equals(entity.getIsPaid())) {
entity.setPaymentDate(LocalDateTime.now());
}
this.setExpenseType(entity);
expenseRepository.save(entity);
}
/**
* sets the Expense type depending on the presence or absence of creditors and debtors
* this is used to filter expenses with a creditor that are paid, since they are not part of
* the actual money the user has spent, it's just a load technically
* Deletes an expense given its ID.
* @param id the ID of the expense to delete
*/
public void delete(Long id) {
expenseRepository.deleteById(id);
}
/**
* Lists expenses in a paginated format.
* @param pageable the pagination information
* @return a page of expenses
*/
public Page<Expense> list(Pageable pageable) {
return expenseRepository.findAll(pageable);
}
/**
* Lists expenses in a paginated format with filtering options.
* @param pageable the pagination information
* @param filter the filter specification
* @return a page of expenses matching the filter
*/
public Page<Expense> list(Pageable pageable, Specification<Expense> filter) {
return expenseRepository.findAll(filter, pageable);
}
/**
* Counts the total number of expenses.
* @return the count of expenses
*/
public int count() {
return (int) expenseRepository.count();
}
// ================================
// Private methods
// ================================
/**
* Sets the expense type depending on the presence or absence of a payer and beneficiary.
* This is used to filter expenses where the payer has been reimbursed.
* @param expense the expense to set the type of
*/
private void setExpenseType(final @Nonnull Expense expense) {
// Check if the creditor is present
if (Objects.nonNull(expense.getCreditor())) {
// If creditor is present, set type to CREDIT
// Check if the payer is present
if (Objects.nonNull(expense.getPayer())) {
// If payer is present, set type to CREDIT
expense.setExpenseType(ExpenseType.CREDIT);
}
// Check if the debtor is present and no creditor
else if (Objects.nonNull(expense.getDebtor())) {
// If debtor is present and no creditor, set type to DEBIT
// Check if the beneficiary is present and no payer
else if (Objects.nonNull(expense.getBeneficiary())) {
// If beneficiary is present and no payer, set type to DEBIT
expense.setExpenseType(ExpenseType.DEBIT);
}
// If neither creditor nor debtor is present
// If neither payer nor beneficiary is present
else {
// If neither creditor nor debtor is present, set type to NONE
// Set type to NONE
expense.setExpenseType(ExpenseType.NONE);
}
}
}

View file

@ -9,7 +9,7 @@ import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@Service
@ -23,60 +23,108 @@ public class PersonService {
this.expenseService = expenseService;
}
/**
* Finds a person by ID.
* @param id the ID of the person
* @return an optional containing the person if found, otherwise empty
*/
public Optional<Person> get(Long id) {
return personRepository.findById(id);
}
public Collection<Person> findAll() {
/**
* Finds all persons.
* @return a collection of all persons
*/
public List<Person> findAll() {
return this.personRepository.findAll();
}
public void update(Person person) {
this.personRepository.save(person);
}
public void delete(Long id) {
this.personRepository.deleteById(id);
}
public Page<Person> list(Pageable pageable){
/**
* Lists all persons with pagination.
* @param pageable the pagination information
* @return a page of persons
*/
public Page<Person> list(Pageable pageable) {
return personRepository.findAll(pageable);
}
/**
* Lists all persons with pagination and filtering.
* @param pageable the pagination information
* @param filter the specification filter
* @return a page of persons matching the filter
*/
public Page<Person> list(Pageable pageable, Specification<Person> filter) {
return this.personRepository.findAll(filter, pageable);
return this.personRepository.findAll(filter, pageable);
}
/**
* Counts the total number of persons.
* @return the total count of persons
*/
public int count() {
return (int) this.personRepository.count();
}
/**
* calculates the debt a certain person has
* @param person the person of which you want to know the debt
* @return the debt that a certain person has
* Updates a person in the repository.
* @param person the person to update
*/
public BigDecimal calculateDebt(final Person person){
return this.expenseService.findDebtByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
public void update(Person person) {
this.personRepository.save(person);
}
/**
* calculates the credit a certain person has
* @param person the person of which you want to know the credit
* @return the credit that a certain person has
* Deletes a person by ID.
* @param id the ID of the person to delete
*/
public void delete(Long id) {
this.personRepository.deleteById(id);
}
/**
* Calculates the total debt of a person.
* @param person the person whose debt is to be calculated
* @return the total debt amount
*/
public BigDecimal calculateDebt(final Person person) {
return this.expenseService.findExpensesWherePayer(person).stream()
.filter(expense -> !expense.getBeneficiary().equals(person) && Boolean.FALSE.equals(expense.getIsPaid()))
.map(Expense::getCost)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* Calculates the total credit of a person.
* @param person the person whose credit is to be calculated
* @return the total credit amount
*/
public BigDecimal calculateCredit(final Person person) {
return this.expenseService.findCreditByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
return this.expenseService.findExpensesWhereBeneficiary(person).stream()
.filter(expense -> !expense.getPayer().equals(person) && Boolean.FALSE.equals(expense.getIsPaid()))
.map(Expense::getCost)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* calculates the balance of a person using the money owed or paid off to that person
* @param person the person of which you want to know the balance
* @return the amount of money owed or paid off to a certain person
* Calculates the net balance of a person.
* The net balance is the difference between the total amount the person is owed
* (expenses where they are the payer) and the total amount the person owes
* (expenses where they are the beneficiary).
*
* A positive net balance means the person is owed money.
* A negative net balance means the person owes money.
*
* @param person the person whose net balance is to be calculated
* @return the net balance amount
*/
public BigDecimal calculateNetBalance(final Person person) {
final var credit = this.expenseService.findUnpaidCreditByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
final var debit = this.expenseService.findUnpaidDebtByUser(person).stream().map(Expense::getCost).reduce(BigDecimal.ZERO, BigDecimal::add);
return credit.subtract(debit);
// Calculate total debt (what others owe to the person)
final BigDecimal debt = this.calculateDebt(person);
// Calculate total credit (what the person owes to others)
final BigDecimal credit = this.calculateCredit(person);
// Net balance calculation: debt (owed to the person) - credit (person owes)
return debt.subtract(credit);
}
}

View file

@ -66,8 +66,8 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
private ComboBox<PeriodUnit> periodUnit;
private TextField periodInterval;
private DatePicker date;
private ComboBox<Person> creditor;
private ComboBox<Person> debtor;
private ComboBox<Person> payer;
private ComboBox<Person> beneficiary;
private ComboBox<Event> event;
public ExpensesView(ExpenseService expenseService, CategoryService categoryService, PersonService personService, EventService eventService, ViewService viewService) {
@ -94,7 +94,6 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
grid.addColumn(Expense::getPeriodUnit).setHeader("Period Unit").setSortable(true);
grid.addColumn(Expense::getDate).setHeader("Date").setSortable(true).setSortProperty("date");
// grid.addColumn(expenseEvent -> expenseEvent.getEvent().getName()).setHeader("Event").setSortable(true);
grid.addColumn(new ComponentRenderer<>(this.viewService::createExpenseBadge)).setHeader("Status").setSortable(true);
grid.getColumns().forEach(col -> col.setAutoWidth(true));
@ -156,11 +155,11 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
//TODO: THIS NEEDS TO BE IMPLEMENTED BUT FOR THE SINGLE PERSON NOW, STILL NEEDED
// // Event listeners that will remove the selected creditors from the debtors list and vice versa
// // Done so that the user cant create an expense with the same person as creditor and debtor
// debtors.addValueChangeListener(event -> {
// Set<Person> selectedDebtors = event.getValue();
// payer.addValueChangeListener(event -> {
// Person selectedDebtors = event.getValue();
// final var creditorsSet = new HashSet<>(personService.findAll());
// creditorsSet.removeIf(selectedDebtors::contains);
// creditors.setItems(creditorsSet);
// creditorsSet.removeIf(creditorsSet.contains(selectedDebtors));
// payer.setItems(creditorsSet);
// });
//
// creditors.addValueChangeListener(event -> {
@ -232,10 +231,10 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
private void createEditorLayout(SplitLayout splitLayout) {
Div editorLayoutDiv = new Div();
editorLayoutDiv.setClassName("editor-layout");
Div editorDiv = new Div();
editorDiv.setClassName("editor");
editorLayoutDiv.add(editorDiv);
final var people = this.personService.findAll();
FormLayout formLayout = new FormLayout();
name = new TextField("Name");
@ -247,15 +246,15 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
periodUnit = new ComboBox<>("Period Unit");
periodUnit.setItems(PeriodUnit.values());
periodInterval = new TextField("Period Interval");
creditor = new ComboBox<>("Creditor");
creditor.setItems(personService.findAll());
creditor.setItemLabelGenerator(Person::getFirstName);
payer = new ComboBox<>("Payer");
payer.setItems(people);
payer.setItemLabelGenerator(Person::getFirstName);
event = new ComboBox<>("Event");
event.setItems(eventService.findAll());
event.setItemLabelGenerator(Event::getName);
debtor = new ComboBox<>("Debtor");
debtor.setItems(personService.findAll());
debtor.setItemLabelGenerator(Person::getFirstName);
beneficiary = new ComboBox<>("Beneficiary");
beneficiary.setItems(people);
beneficiary.setItemLabelGenerator(Person::getFirstName);
date = new DatePicker("Date");
// Horizontal layout for checkboxes
@ -264,7 +263,7 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
isPaid = new Checkbox("Paid");
checkboxLayout.add(isPeriodic, isPaid);
formLayout.add(name, cost, category, description, checkboxLayout, periodUnit, periodInterval, date, creditor, debtor, event);
formLayout.add(name, cost, category, description, checkboxLayout, periodUnit, periodInterval, date, payer, beneficiary, event);
editorDiv.add(formLayout);
createButtonLayout(editorLayoutDiv);
@ -304,9 +303,5 @@ public class ExpensesView extends Div implements BeforeEnterObserver {
boolean isPeriodicChecked = (value != null) && value.getIsPeriodic();
periodUnit.setVisible(isPeriodicChecked);
periodInterval.setVisible(isPeriodicChecked);
// Set selected items for creditor and debtor
creditor.setValue(value != null ? value.getCreditor() : null);
debtor.setValue(value != null ? value.getDebtor() : null);
}
}

View file

@ -83,9 +83,9 @@ public class PeopleView extends Div implements BeforeEnterObserver {
else return this.viewService.createExpenseBadge(((Expense) persona));
})).setHeader("Balance Status");
grid.addColumn(new ComponentRenderer<>(person -> {
if (person instanceof Person) {
Button markPaidButton = new Button("Mark All Expenses Paid", event -> markExpensesPaid((Person) person));
grid.addColumn(new ComponentRenderer<>(persona -> {
if (persona instanceof Person) {
Button markPaidButton = new Button("Mark All Expenses Paid", event -> markExpensesPaid((Person) persona));
markPaidButton.addThemeVariants(ButtonVariant.LUMO_SMALL, ButtonVariant.LUMO_PRIMARY);
return markPaidButton;
} else {
@ -93,7 +93,7 @@ public class PeopleView extends Div implements BeforeEnterObserver {
}
})).setHeader("Actions");
List<Person> people = (List<Person>) personService.findAll();
List<Person> people = personService.findAll();
this.setGridData(people);
@ -248,7 +248,7 @@ public class PeopleView extends Div implements BeforeEnterObserver {
grid.getTreeData().addItem(null, user);
// Fetch expenses for the current person
List<Expense> expenses = expenseService.findExpenseByUser(user);
List<Expense> expenses = expenseService.findExpensesByUser(user);
// Add each expense as a child item under the person
for (Expense expense : expenses) grid.getTreeData().addItem(user, expense);
@ -257,7 +257,7 @@ public class PeopleView extends Div implements BeforeEnterObserver {
private void markExpensesPaid(Person person) {
try {
List<Expense> expenses = expenseService.findCreditByUser(person).stream().toList();
List<Expense> expenses = expenseService.findExpensesWherePayer(person).stream().toList();
for (Expense expense : expenses) {
expense.setIsPaid(true);
expenseService.update(expense);