diff --git a/src/main/java/com/satwik/splitora/controller/GroupController.java b/src/main/java/com/satwik/splitora/controller/GroupController.java index 817aad8..971e58d 100644 --- a/src/main/java/com/satwik/splitora/controller/GroupController.java +++ b/src/main/java/com/satwik/splitora/controller/GroupController.java @@ -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; @@ -179,4 +180,22 @@ public ResponseEntity> 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> getBalanceTransactions(@PathVariable UUID groupId) { + log.info("Get Endpoint: get balance transactions for group with id: {}", groupId); + BalanceTransaction transactions = groupService.getBalanceTransactions(groupId); + ResponseModel 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); + } } \ No newline at end of file diff --git a/src/main/java/com/satwik/splitora/persistence/dto/group/BalanceTransaction.java b/src/main/java/com/satwik/splitora/persistence/dto/group/BalanceTransaction.java new file mode 100644 index 0000000..c2b329e --- /dev/null +++ b/src/main/java/com/satwik/splitora/persistence/dto/group/BalanceTransaction.java @@ -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> transactions; +} diff --git a/src/main/java/com/satwik/splitora/persistence/entities/BaseEntity.java b/src/main/java/com/satwik/splitora/persistence/entities/BaseEntity.java index 28c3d07..f863135 100644 --- a/src/main/java/com/satwik/splitora/persistence/entities/BaseEntity.java +++ b/src/main/java/com/satwik/splitora/persistence/entities/BaseEntity.java @@ -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; @@ -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; diff --git a/src/main/java/com/satwik/splitora/persistence/entities/Expense.java b/src/main/java/com/satwik/splitora/persistence/entities/Expense.java index b710dc1..e3eb0a2 100644 --- a/src/main/java/com/satwik/splitora/persistence/entities/Expense.java +++ b/src/main/java/com/satwik/splitora/persistence/entities/Expense.java @@ -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; diff --git a/src/main/java/com/satwik/splitora/persistence/entities/Group.java b/src/main/java/com/satwik/splitora/persistence/entities/Group.java index d459731..8437929 100644 --- a/src/main/java/com/satwik/splitora/persistence/entities/Group.java +++ b/src/main/java/com/satwik/splitora/persistence/entities/Group.java @@ -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; diff --git a/src/main/java/com/satwik/splitora/repository/ExpenseRepository.java b/src/main/java/com/satwik/splitora/repository/ExpenseRepository.java index 7b71ef5..58a812c 100644 --- a/src/main/java/com/satwik/splitora/repository/ExpenseRepository.java +++ b/src/main/java/com/satwik/splitora/repository/ExpenseRepository.java @@ -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; @@ -11,4 +13,13 @@ public interface ExpenseRepository extends JpaRepository { List 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> findTransactionsByGroupId(UUID id); } diff --git a/src/main/java/com/satwik/splitora/service/implementations/ExpenseServiceImpl.java b/src/main/java/com/satwik/splitora/service/implementations/ExpenseServiceImpl.java index c3ecdaf..9d03f7c 100644 --- a/src/main/java/com/satwik/splitora/service/implementations/ExpenseServiceImpl.java +++ b/src/main/java/com/satwik/splitora/service/implementations/ExpenseServiceImpl.java @@ -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(); @@ -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; } @@ -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; } diff --git a/src/main/java/com/satwik/splitora/service/implementations/GroupServiceImpl.java b/src/main/java/com/satwik/splitora/service/implementations/GroupServiceImpl.java index 13c7627..45d0a82 100644 --- a/src/main/java/com/satwik/splitora/service/implementations/GroupServiceImpl.java +++ b/src/main/java/com/satwik/splitora/service/implementations/GroupServiceImpl.java @@ -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.*; @@ -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; @@ -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); @@ -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)); @@ -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> transactions = expenseRepository.findTransactionsByGroupId(group.getId()); + SettlementStrategy settlementStrategy = new SortSettlementStrategy<>(); + List> balancingTransaction = settlementStrategy.settle(transactions); + BalanceTransaction balanceTransaction = new BalanceTransaction(); + balanceTransaction.setGroupId(group.getId()); + balanceTransaction.setGroupName(group.getGroupName()); + balanceTransaction.setTransactions(balancingTransaction); + + return balanceTransaction; + } } diff --git a/src/main/java/com/satwik/splitora/service/interfaces/GroupService.java b/src/main/java/com/satwik/splitora/service/interfaces/GroupService.java index f7a55cc..0e5eb82 100644 --- a/src/main/java/com/satwik/splitora/service/interfaces/GroupService.java +++ b/src/main/java/com/satwik/splitora/service/interfaces/GroupService.java @@ -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; @@ -24,4 +25,6 @@ public interface GroupService { List findMembers(UUID groupId); String deleteMembers(UUID groupId, UUID groupMemberId); + + BalanceTransaction getBalanceTransactions(UUID groupId); } diff --git a/src/main/java/com/satwik/splitora/settlement/model/Transaction.java b/src/main/java/com/satwik/splitora/settlement/model/Transaction.java new file mode 100644 index 0000000..fb01ac9 --- /dev/null +++ b/src/main/java/com/satwik/splitora/settlement/model/Transaction.java @@ -0,0 +1,17 @@ +package com.satwik.splitora.settlement.model; + +import lombok.Data; + +@Data +public class Transaction { + + U debtor; + U creditor; + Double amount; + + public Transaction(U debtor, U creditor, Double amount) { + this.debtor = debtor; + this.creditor = creditor; + this.amount = amount; + } +} diff --git a/src/main/java/com/satwik/splitora/settlement/strategy/SettlementStrategy.java b/src/main/java/com/satwik/splitora/settlement/strategy/SettlementStrategy.java new file mode 100644 index 0000000..2c06841 --- /dev/null +++ b/src/main/java/com/satwik/splitora/settlement/strategy/SettlementStrategy.java @@ -0,0 +1,17 @@ +package com.satwik.splitora.settlement.strategy; + +import com.satwik.splitora.settlement.model.Transaction; + +import java.util.List; + +public interface SettlementStrategy { + + /** + * Settle a list of transactions and return the settled transactions. + * + * @param transactions List of transactions to be settled + * @return List of settled transactions + */ + List> settle(List> transactions); + +} diff --git a/src/main/java/com/satwik/splitora/settlement/strategy/SortSettlementStrategy.java b/src/main/java/com/satwik/splitora/settlement/strategy/SortSettlementStrategy.java new file mode 100644 index 0000000..bf5ec8e --- /dev/null +++ b/src/main/java/com/satwik/splitora/settlement/strategy/SortSettlementStrategy.java @@ -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 implements SettlementStrategy { + + @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> settle(List> transactions) { + HashMap balances = new HashMap<>(); + for (Transaction 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 creditors = new ArrayList<>(); + List 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> 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; + } +}