Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/main/java/com/satwik/splitora/controller/GroupController.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.satwik.splitora.controller;

import com.satwik.splitora.persistence.dto.ResponseModel;
import com.satwik.splitora.persistence.dto.group.BalanceTransaction;
import com.satwik.splitora.persistence.dto.group.GroupDTO;
import com.satwik.splitora.persistence.dto.group.GroupListDTO;
import com.satwik.splitora.persistence.dto.group.GroupUpdateRequest;
Expand Down Expand Up @@ -179,4 +180,22 @@ public ResponseEntity<ResponseModel<String>> deleteMembers(@PathVariable UUID gr
log.info("Delete Endpoint: delete a member with response: {}", responseModel);
return ResponseEntity.status(HttpStatus.OK).body(responseModel);
}

/**
* Retrieves balance transactions for a group.
*
* This endpoint processes the request to get balance transactions for a group identified by the given group ID.
* It logs the incoming request and the resulting response.
*
* @param groupId the UUID of the group for which balance transactions are to be retrieved.
* @return a ResponseEntity containing a BalanceTransaction object with the group's balance transactions.
*/
@GetMapping("/getBalanceTransactions/{groupId}")
public ResponseEntity<ResponseModel<BalanceTransaction>> getBalanceTransactions(@PathVariable UUID groupId) {
log.info("Get Endpoint: get balance transactions for group with id: {}", groupId);
BalanceTransaction transactions = groupService.getBalanceTransactions(groupId);
ResponseModel<BalanceTransaction> responseModel = ResponseUtil.success(transactions, HttpStatus.OK, "Balance transactions retrieved successfully");
log.info("Get Endpoint: get balance transactions response: {}", responseModel);
return ResponseEntity.status(HttpStatus.OK).body(responseModel);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.satwik.splitora.persistence.dto.group;

import com.satwik.splitora.settlement.model.Transaction;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;
import java.util.UUID;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class BalanceTransaction {

private UUID groupId;
private String groupName;
private List<Transaction<UUID>> transactions;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.satwik.splitora.persistence.entities;

import jakarta.persistence.Column;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
Expand All @@ -18,8 +19,9 @@
public class BaseEntity {

@Id
@GenericGenerator(name = "uuid")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
@GeneratedValue(generator = "uuid")
@Column(columnDefinition = "BINARY(16)")
private UUID id;

private UUID createdBy;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class Expense extends BaseEntity {
@JoinColumn(name = "group_id")
private Group group;

@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "payer_id")
private User payer;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class Group extends BaseEntity {
@Column(name = "default_group")
private boolean defaultGroup;

@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private User user;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.satwik.splitora.repository;

import com.satwik.splitora.persistence.entities.Expense;
import com.satwik.splitora.settlement.model.Transaction;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;
Expand All @@ -11,4 +13,13 @@
public interface ExpenseRepository extends JpaRepository<Expense, UUID> {
List<Expense> findByGroupId(UUID groupId);

@Query("SELECT NEW com.satwik.splitora.settlement.model.Transaction("+
"es.user.id, " +
"e.payer.id, " +
"es.sharedAmount) " +
"FROM Expense e " +
"JOIN ExpenseShare es " +
"ON e.id = es.expense.id " +
"WHERE e.group.id = ?1 ")
List<Transaction<UUID>> findTransactionsByGroupId(UUID id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ public ExpenseDTO createGroupedExpense(UUID groupId, ExpenseDTO expenseDTO) {
User payer = expenseDTO.getPayerId() != null ? userRepository.findById(expenseDTO.getPayerId()).orElseThrow(() -> new DataNotFoundException("Payer not found!")) : authorizationService.getAuthorizedUser();
Group group = groupRepository.findById(groupId).orElseThrow(() -> new DataNotFoundException(ErrorMessages.GROUP_NOT_FOUND));

// checking if payer is a member of the group
if(!groupMembersRepository.existsByGroupIdAndMemberId(group.getId(), payer.getId()))
// checking if payer is a member of the group or owner of the group
if(group.getUser().getId() != payer.getId() && !groupMembersRepository.existsByGroupIdAndMemberId(group.getId(), payer.getId()))
throw new BadRequestException("Payer is not a member of this group!");

Expense expense = new Expense();
Expand All @@ -70,6 +70,7 @@ public ExpenseDTO createGroupedExpense(UUID groupId, ExpenseDTO expenseDTO) {
response.setPayerId(expense.getPayer().getId());
response.setDescription(expense.getDescription());
response.setPayerName(expense.getPayer().getUsername());
response.setDate(expense.getCreatedOn());
return response;
}

Expand All @@ -91,9 +92,10 @@ public ExpenseDTO createNonGroupedExpense(ExpenseDTO expenseDTO) {
ExpenseDTO response = new ExpenseDTO();
response.setExpenseId(expense.getId());
response.setAmount(expense.getAmount());
response.setPayerId(expense.getPayer().getId());
response.setDescription(expense.getDescription());
response.setPayerName(expense.getPayer().getUsername());
response.setPayerId(expense.getPayer().getId());
response.setDate(expense.getCreatedOn());

return response;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.satwik.splitora.service.implementations;

import com.satwik.splitora.constants.ErrorMessages;
import com.satwik.splitora.exception.BadRequestException;
import com.satwik.splitora.exception.DataNotFoundException;
import com.satwik.splitora.persistence.dto.expense.ExpenseListDTO;
import com.satwik.splitora.persistence.dto.group.*;
Expand All @@ -15,6 +16,9 @@
import com.satwik.splitora.repository.GroupRepository;
import com.satwik.splitora.repository.UserRepository;
import com.satwik.splitora.service.interfaces.GroupService;
import com.satwik.splitora.settlement.model.Transaction;
import com.satwik.splitora.settlement.strategy.SettlementStrategy;
import com.satwik.splitora.settlement.strategy.SortSettlementStrategy;
import jakarta.transaction.Transactional;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
Expand Down Expand Up @@ -83,7 +87,11 @@ public GroupListDTO findAllGroup() {
public String addGroupMembers(UUID groupId, UUID memberId) {
Group group = groupRepository.findById(groupId).orElseThrow(() -> new DataNotFoundException(ErrorMessages.GROUP_NOT_FOUND));
User member = userRepository.findById(memberId).orElseThrow(() -> new DataNotFoundException("User not found to add as member!"));
// TODO : add check to avoid add members to default group

if(group.isDefaultGroup()) {
throw new BadRequestException("You can't add members to the default group!");
}

GroupMembers groupMembers = new GroupMembers();
groupMembers.setMember(member);
groupMembers.setGroup(group);
Expand Down Expand Up @@ -131,6 +139,7 @@ public String deleteGroupByGroupId(UUID groupId) {
}

@Override
@Transactional
@PreAuthorize("@authorizationService.isGroupOwner(#groupId)")
public GroupDTO findGroupByGroupId(UUID groupId) {
Group group = groupRepository.findById(groupId).orElseThrow(() -> new DataNotFoundException(ErrorMessages.GROUP_NOT_FOUND));
Expand Down Expand Up @@ -186,4 +195,21 @@ public String updateGroup(GroupUpdateRequest groupUpdateRequest, UUID groupId) {
groupRepository.save(group);
return "%s - Group update successfully!".formatted(group.getId());
}

@Override
@Transactional
@PreAuthorize("@authorizationService.isGroupOwner(#groupId)")
public BalanceTransaction getBalanceTransactions(UUID groupId) {
Group group = groupRepository.findById(groupId).orElseThrow(() -> new DataNotFoundException(ErrorMessages.GROUP_NOT_FOUND));
// Fetch all the transactions related to the group
List<Transaction<UUID>> transactions = expenseRepository.findTransactionsByGroupId(group.getId());
SettlementStrategy<UUID> settlementStrategy = new SortSettlementStrategy<>();
List<Transaction<UUID>> balancingTransaction = settlementStrategy.settle(transactions);
BalanceTransaction balanceTransaction = new BalanceTransaction();
balanceTransaction.setGroupId(group.getId());
balanceTransaction.setGroupName(group.getGroupName());
balanceTransaction.setTransactions(balancingTransaction);

return balanceTransaction;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.satwik.splitora.service.interfaces;

import com.satwik.splitora.persistence.dto.group.BalanceTransaction;
import com.satwik.splitora.persistence.dto.group.GroupDTO;
import com.satwik.splitora.persistence.dto.group.GroupListDTO;
import com.satwik.splitora.persistence.dto.group.GroupUpdateRequest;
Expand All @@ -24,4 +25,6 @@ public interface GroupService {
List<UserDTO> findMembers(UUID groupId);

String deleteMembers(UUID groupId, UUID groupMemberId);

BalanceTransaction getBalanceTransactions(UUID groupId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.satwik.splitora.settlement.model;

import lombok.Data;

@Data
public class Transaction<U> {

U debtor;
U creditor;
Double amount;

public Transaction(U debtor, U creditor, Double amount) {
this.debtor = debtor;
this.creditor = creditor;
this.amount = amount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.satwik.splitora.settlement.strategy;

import com.satwik.splitora.settlement.model.Transaction;

import java.util.List;

public interface SettlementStrategy <U> {

/**
* Settle a list of transactions and return the settled transactions.
*
* @param transactions List of transactions to be settled
* @return List of settled transactions
*/
List<Transaction<U>> settle(List<Transaction<U>> transactions);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.satwik.splitora.settlement.strategy;

import com.satwik.splitora.settlement.model.Transaction;
import lombok.Data;

import java.util.*;

public class SortSettlementStrategy<U> implements SettlementStrategy<U> {

@Data
private class Person {
private U id;
private Double balance;

public Person(U id, Double balance) {
this.id = id;
this.balance = balance;
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(id, person.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}
}

@Override
public List<Transaction<U>> settle(List<Transaction<U>> transactions) {
HashMap<U, Double> balances = new HashMap<>();
for (Transaction<U> transaction : transactions) {
U creditor = transaction.getCreditor();
U debtor = transaction.getDebtor();
double amount = transaction.getAmount();

balances.put(creditor, balances.getOrDefault(creditor, 0.0) + amount);
balances.put(debtor, balances.getOrDefault(debtor, 0.0) - amount);
}

// Create lists for creditors and debtors
List<Person> creditors = new ArrayList<>();
List<Person> debtors = new ArrayList<>();

for (var entry : balances.entrySet()) {
U personId = entry.getKey();
Double balance = entry.getValue();

if (balance < 0) {
creditors.add(new Person(personId, balance));
} else if (balance > 0) {
debtors.add(new Person(personId, balance));
}
}

creditors.sort(Comparator.comparingDouble(Person::getBalance));
debtors.sort(Comparator.comparingDouble(Person::getBalance));

List<Transaction<U>> settlingTransactions = new ArrayList<>();

int i = 0;
int j = 0;
while (i < creditors.size() && j < debtors.size()) {
Person creditor = creditors.get(i);
Person debtor = debtors.get(j);

double minAmount = Math.min(creditor.balance, debtor.balance);
settlingTransactions.add(new Transaction<>(debtor.getId(), creditor.getId(), Math.abs(minAmount)));

creditor.balance -= minAmount;
debtor.balance -= minAmount;

if (creditor.balance == 0) i++;
if (debtor.balance == 0) j++;
}

return settlingTransactions;
}
}