Skip to content

Commit

Permalink
FIN-353 extract the budget expense to an owned class. Ensuring that i…
Browse files Browse the repository at this point in the history
…t can only exist in context of a budget.
  • Loading branch information
gjong committed Mar 5, 2024
1 parent 193d3a2 commit d45e655
Show file tree
Hide file tree
Showing 16 changed files with 138 additions and 219 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import jakarta.inject.Inject;
import org.assertj.core.api.Assertions;
import org.camunda.bpm.engine.variable.Variables;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
Expand All @@ -26,16 +27,7 @@ public class BudgetAnalysisIT {
@DisplayName("Budget analysis without a recorded deviation")
void budgetWithoutDeviation(RuntimeContext context) {
context
.withBudget(2019, 1, Budget.builder()
.expenses(Collections.List(
Budget.Expense.builder()
.id(1L)
.lowerBound(75)
.upperBound(100)
.name("Groceries")
.build()))
.id(1L)
.build())
.withBudget(2019, 1, createBudget())
.withTransactionPages()
.thenReturn(ResultPage.of(
buildTransaction(50.2, "Groceries", "My Account", "To Account"),
Expand Down Expand Up @@ -67,19 +59,11 @@ void budgetWithoutDeviation(RuntimeContext context) {
}

@Test
@Disabled
@DisplayName("Budget analysis with a recorded deviation")
void budgetWithDeviation(RuntimeContext context) {
context
.withBudget(2019, 1, Budget.builder()
.expenses(Collections.List(
Budget.Expense.builder()
.id(1L)
.lowerBound(75)
.upperBound(100)
.name("Groceries")
.build()))
.id(1L)
.build())
.withBudget(2019, 1, createBudget())
.withTransactionPages()
.thenReturn(ResultPage.of(
buildTransaction(50.2, "Groceries", "My Account", "To Account"),
Expand Down Expand Up @@ -110,6 +94,15 @@ void budgetWithDeviation(RuntimeContext context) {
.verifyVariable("needed_correction", a -> Assertions.assertThat(a).isEqualTo(-14.23));
}

private Budget createBudget() {
var budget = Budget.builder()
.id(1L)
.build();
budget.new Expense(1L, "Groceries", 100);

return budget;
}

public static Transaction buildTransaction(double amount, String description, String to, String from) {
return Transaction.builder()
.description(description)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.jongsoft.finance.core.DateUtils;
import com.jongsoft.finance.domain.account.Account;
import com.jongsoft.finance.domain.transaction.Transaction;
import com.jongsoft.finance.domain.user.Budget;
import com.jongsoft.finance.domain.user.Role;
import com.jongsoft.finance.domain.user.UserAccount;
import com.jongsoft.finance.factory.FilterFactory;
Expand All @@ -22,8 +21,6 @@

class ProcessBudgetAnalysisDelegateTest {

private FilterFactory filterFactory;
private SettingProvider applicationSettings;
private TransactionProvider transactionProvider;
private DelegateExecution execution;

Expand All @@ -35,9 +32,9 @@ class ProcessBudgetAnalysisDelegateTest {
void setup() {
execution = Mockito.mock(DelegateExecution.class);
transactionProvider = Mockito.mock(TransactionProvider.class);
applicationSettings = Mockito.mock(SettingProvider.class);
filterFactory = Mockito.mock(FilterFactory.class);
filterCommand = Mockito.mock(TransactionProvider.FilterCommand.class, InvocationOnMock::getMock);
var applicationSettings = Mockito.mock(SettingProvider.class);
var filterFactory = Mockito.mock(FilterFactory.class);

subject = new ProcessBudgetAnalysisDelegate(transactionProvider, filterFactory, applicationSettings);

Expand All @@ -49,12 +46,12 @@ void setup() {
.build());

Mockito.when(execution.getVariableLocal("date")).thenReturn(LocalDate.of(2019, 1, 23));
Mockito.when(execution.getVariableLocal("expense")).thenReturn(Budget.Expense.builder()
.lowerBound(100)
.upperBound(110)
.id(1L)
.name("test expense")
.build());
// Mockito.when(execution.getVariableLocal("expense")).thenReturn(Budget.Expense.builder()
// .lowerBound(100)
// .upperBound(110)
// .id(1L)
// .name("test expense")
// .build());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ class ProcessBudgetCreateDelegateTest {
private BudgetProvider budgetProvider;
private DelegateExecution execution;
private ApplicationEventPublisher eventPublisher;
private CurrentUserProvider currentUserFacade;

private ProcessBudgetCreateDelegate subject;

Expand All @@ -39,7 +38,7 @@ void setup() {
budgetProvider = Mockito.mock(BudgetProvider.class);
execution = Mockito.mock(DelegateExecution.class);
eventPublisher = Mockito.mock(ApplicationEventPublisher.class);
currentUserFacade = Mockito.mock(CurrentUserProvider.class);
var currentUserFacade = Mockito.mock(CurrentUserProvider.class);

subject = new ProcessBudgetCreateDelegate(currentUserFacade, budgetProvider, TestUtilities.getProcessMapper());

Expand Down Expand Up @@ -89,15 +88,8 @@ void execute_indexation() {
.id(1L)
.start(LocalDate.of(2018, 1, 1))
.expectedIncome(1100)
.expenses(Collections.List(
Budget.Expense.builder()
.id(1L)
.name("Expense 1")
.lowerBound(10)
.upperBound(20)
.build()
))
.build());
initial.new Expense(1, "Expense 1", 15);

Mockito.when(budgetProvider.lookup(2019, 1)).thenReturn(Control.Option(initial));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.jongsoft.finance.bpmn.delegate.budget;

import com.jongsoft.finance.ResultPage;
import com.jongsoft.finance.domain.core.EntityRef;
import com.jongsoft.finance.factory.FilterFactory;
import com.jongsoft.finance.providers.ExpenseProvider;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import com.jongsoft.finance.factory.FilterFactory;
import com.jongsoft.finance.ResultPage;
import com.jongsoft.finance.domain.user.Budget;
import com.jongsoft.finance.providers.ExpenseProvider;

class ProcessBudgetLookupDelegateTest {

Expand All @@ -33,15 +33,13 @@ void setup() {

@Test
void execute() {
Budget.Expense budget = Budget.Expense.builder().build();

Mockito.when(execution.getVariableLocal("name")).thenReturn("Group 1");
Mockito.when(expenseProvider.lookup(Mockito.any(ExpenseProvider.FilterCommand.class)))
.thenReturn(ResultPage.of(budget));
.thenReturn(ResultPage.of(new EntityRef.NamedEntity(1, "Must have")));

subject.execute(execution);

Mockito.verify(execution).setVariable("budget", budget);
Mockito.verify(execution).setVariable("budget", new EntityRef.NamedEntity(1, "Must have"));
Mockito.verify(filterCommand).name("Group 1", true);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.jongsoft.finance.domain.core;

import com.jongsoft.finance.core.AggregateBase;
import io.micronaut.serde.annotation.Serdeable;
import lombok.EqualsAndHashCode;
import lombok.Getter;

Expand All @@ -14,4 +15,12 @@ public EntityRef(Long id) {
this.id = id;
}

@Serdeable
public record NamedEntity(long id, String name) implements AggregateBase {
@Override
public Long getId() {
return id;
}
}

}
80 changes: 35 additions & 45 deletions domain/src/main/java/com/jongsoft/finance/domain/user/Budget.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
import com.jongsoft.finance.annotation.Aggregate;
import com.jongsoft.finance.annotation.BusinessMethod;
import com.jongsoft.finance.core.AggregateBase;
import com.jongsoft.finance.core.exception.StatusException;
import com.jongsoft.finance.messaging.EventBus;
import com.jongsoft.finance.messaging.commands.budget.CloseBudgetCommand;
import com.jongsoft.finance.messaging.commands.budget.CreateBudgetCommand;
import com.jongsoft.finance.messaging.commands.budget.CreateExpenseCommand;
import com.jongsoft.finance.messaging.commands.budget.UpdateExpenseCommand;
import com.jongsoft.lang.Collections;
import com.jongsoft.lang.collection.Sequence;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.*;

import java.math.BigDecimal;
import java.math.MathContext;
Expand All @@ -27,11 +26,11 @@
public class Budget implements AggregateBase {

@Getter
@Builder
@AllArgsConstructor
public static class Expense implements AggregateBase {
@ToString(of = "name")
@EqualsAndHashCode(of = "id")
public class Expense implements AggregateBase {
private Long id;
private String name;
private final String name;
private double lowerBound;
private double upperBound;

Expand All @@ -45,23 +44,24 @@ public static class Expense implements AggregateBase {
this.upperBound = upperBound;
}

Expense indexExpense(BigDecimal deviation) {
return Expense.builder()
.id(id)
.name(name)
.lowerBound(BigDecimal.valueOf(lowerBound)
.multiply(deviation)
.setScale(0, RoundingMode.CEILING)
.doubleValue())
.upperBound(BigDecimal.valueOf(upperBound)
.multiply(deviation)
.setScale(0, RoundingMode.CEILING)
.doubleValue())
.build();
/**
* Create an expense and bind it to its parent budget.
* This will not register the expense in the system yet.
*/
public Expense(long id, String name, double amount) {
this.id = id;
this.name = name;
this.upperBound = amount;
this.lowerBound = amount - 0.01;
expenses = expenses.append(this);
}

@BusinessMethod
public void updateExpense(double expectedExpense) {
if (computeExpenses() + expectedExpense > expectedIncome) {
throw StatusException.badRequest("Expected expenses exceeds the expected income.");
}

lowerBound = expectedExpense - .01;
upperBound = expectedExpense;

Expand All @@ -78,39 +78,21 @@ public double computeBudget() {
.setScale(2, RoundingMode.HALF_UP)
.doubleValue();
}

@Override
public boolean equals(Object obj) {
if (obj instanceof Expense other) {
return other.getId().equals(getId());
}

return false;
}

@Override
public int hashCode() {
return 7 + id.hashCode();
}

@Override
public String toString() {
return getName();
}
}

private Long id;
private LocalDate start;
private LocalDate end;

private Sequence<Expense> expenses;
@Builder.Default
private Sequence<Expense> expenses = Collections.List();
private double expectedIncome;

private transient boolean active;

Budget(LocalDate start, double expectedIncome) {
if (expectedIncome < 1) {
throw new IllegalStateException("Expected income cannot be less than 1.");
throw StatusException.internalError("Expected income cannot be less than 1.");
}

this.start = start;
Expand All @@ -129,7 +111,15 @@ public Budget indexBudget(LocalDate perDate, double expectedIncome) {
.divide(BigDecimal.valueOf(this.expectedIncome), 20, RoundingMode.HALF_UP));

var newBudget = new Budget(perDate, expectedIncome);
newBudget.expenses = expenses.map(e -> e.indexExpense(deviation));
for (var expense : expenses) {
newBudget.new Expense(
expense.id,
expense.name,
BigDecimal.valueOf(expense.computeBudget())
.multiply(deviation)
.setScale(0, RoundingMode.CEILING)
.doubleValue());
}
newBudget.activate();

return newBudget;
Expand All @@ -141,11 +131,11 @@ public Budget indexBudget(LocalDate perDate, double expectedIncome) {
@BusinessMethod
public void createExpense(String name, double lowerBound, double upperBound) {
if (end != null) {
throw new IllegalStateException("Cannot add expense to an already closed budget period.");
throw StatusException.badRequest("Cannot add expense to an already closed budget period.");
}

if (computeExpenses() + upperBound > expectedIncome) {
throw new IllegalStateException("Expected expenses exceeds the expected income.");
throw StatusException.badRequest("Expected expenses exceeds the expected income.");
}

expenses = expenses.append(new Expense(name, lowerBound, upperBound));
Expand All @@ -161,7 +151,7 @@ void activate() {

void close(LocalDate endDate) {
if (this.end != null) {
throw new IllegalStateException("Already closed budget cannot be closed again.");
throw StatusException.badRequest("Already closed budget cannot be closed again.");
}

this.end = endDate;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package com.jongsoft.finance.providers;

import com.jongsoft.finance.ResultPage;
import com.jongsoft.finance.domain.user.Budget;
import com.jongsoft.finance.domain.core.EntityRef;

public interface ExpenseProvider extends DataProvider<Budget.Expense> {
public interface ExpenseProvider extends DataProvider<EntityRef.NamedEntity> {

interface FilterCommand {
FilterCommand name(String value, boolean exact);
}

ResultPage<Budget.Expense> lookup(FilterCommand filter);
ResultPage<EntityRef.NamedEntity> lookup(FilterCommand filter);

default boolean supports(Class<Budget.Expense> supportingClass) {
return Budget.Expense.class.equals(supportingClass);
default boolean supports(Class<EntityRef.NamedEntity> supportingClass) {
return EntityRef.NamedEntity.class.equals(supportingClass);
}
}
Loading

0 comments on commit d45e655

Please sign in to comment.