diff --git a/server/src/main/java/io/myfinbox/expense/ExpenseCreated.java b/server/src/main/java/io/myfinbox/expense/ExpenseCreated.java index 1498701..3bb3406 100644 --- a/server/src/main/java/io/myfinbox/expense/ExpenseCreated.java +++ b/server/src/main/java/io/myfinbox/expense/ExpenseCreated.java @@ -8,6 +8,7 @@ import java.time.LocalDate; import java.util.UUID; +import static io.myfinbox.shared.Guards.notBlank; import static io.myfinbox.shared.Guards.notNull; /** @@ -22,17 +23,19 @@ public record ExpenseCreated(UUID expenseId, UUID categoryId, MonetaryAmount amount, LocalDate expenseDate, - PaymentType paymentType) implements DomainEvent { + PaymentType paymentType, + String categoryName) implements DomainEvent { /** * Constructor for the ExpenseCreated record. * - * @param expenseId The unique identifier of the expense. - * @param accountId The identifier of the account associated with the expense. - * @param categoryId The identifier of the category associated with the expense. - * @param amount The amount of the expense. - * @param expenseDate The date of the expense. - * @param paymentType The payment type of the expense. + * @param expenseId The unique identifier of the expense. + * @param accountId The identifier of the account associated with the expense. + * @param categoryId The identifier of the category associated with the expense. + * @param amount The amount of the expense. + * @param expenseDate The date of the expense. + * @param paymentType The payment type of the expense. + * @param categoryName The category name associated with the expense. */ public ExpenseCreated { notNull(expenseId, "expenseId cannot be null."); @@ -41,5 +44,6 @@ public record ExpenseCreated(UUID expenseId, notNull(amount, "amount cannot be null."); notNull(expenseDate, "expenseDate cannot be null."); notNull(paymentType, "paymentType cannot be null."); + notBlank(categoryName, "categoryName cannot be blank."); } } diff --git a/server/src/main/java/io/myfinbox/expense/ExpenseDeleted.java b/server/src/main/java/io/myfinbox/expense/ExpenseDeleted.java index 6b5f8ed..81963be 100644 --- a/server/src/main/java/io/myfinbox/expense/ExpenseDeleted.java +++ b/server/src/main/java/io/myfinbox/expense/ExpenseDeleted.java @@ -8,6 +8,7 @@ import java.time.LocalDate; import java.util.UUID; +import static io.myfinbox.shared.Guards.notBlank; import static io.myfinbox.shared.Guards.notNull; /** @@ -22,17 +23,19 @@ public record ExpenseDeleted(UUID expenseId, UUID categoryId, MonetaryAmount amount, LocalDate expenseDate, - PaymentType paymentType) implements DomainEvent { + PaymentType paymentType, + String categoryName) implements DomainEvent { /** * Constructor for the ExpenseDeleted record. * - * @param expenseId The unique identifier of the expense. - * @param accountId The identifier of the account associated with the expense. - * @param categoryId The identifier of the category associated with the expense. - * @param amount The amount of the expense. - * @param expenseDate The date of the expense. - * @param paymentType The payment type of the expense. + * @param expenseId The unique identifier of the expense. + * @param accountId The identifier of the account associated with the expense. + * @param categoryId The identifier of the category associated with the expense. + * @param amount The amount of the expense. + * @param expenseDate The date of the expense. + * @param paymentType The payment type of the expense. + * @param categoryName The category name associated with the expense. */ public ExpenseDeleted { notNull(expenseId, "expenseId cannot be null."); @@ -41,5 +44,6 @@ public record ExpenseDeleted(UUID expenseId, notNull(amount, "amount cannot be null."); notNull(expenseDate, "expenseDate cannot be null."); notNull(paymentType, "paymentType cannot be null."); + notBlank(categoryName, "categoryName cannot be blank."); } } diff --git a/server/src/main/java/io/myfinbox/expense/ExpenseUpdated.java b/server/src/main/java/io/myfinbox/expense/ExpenseUpdated.java index dfe803b..5b9eec6 100644 --- a/server/src/main/java/io/myfinbox/expense/ExpenseUpdated.java +++ b/server/src/main/java/io/myfinbox/expense/ExpenseUpdated.java @@ -8,6 +8,7 @@ import java.time.LocalDate; import java.util.UUID; +import static io.myfinbox.shared.Guards.notBlank; import static io.myfinbox.shared.Guards.notNull; /** @@ -22,17 +23,19 @@ public record ExpenseUpdated(UUID expenseId, UUID categoryId, MonetaryAmount amount, LocalDate expenseDate, - PaymentType paymentType) implements DomainEvent { + PaymentType paymentType, + String categoryName) implements DomainEvent { /** * Constructor for the ExpenseUpdated record. * - * @param expenseId The unique identifier of the expense. - * @param accountId The identifier of the account associated with the expense. - * @param categoryId The identifier of the category associated with the expense. - * @param amount The amount of the expense. - * @param expenseDate The date of the expense. - * @param paymentType The payment type of the expense. + * @param expenseId The unique identifier of the expense. + * @param accountId The identifier of the account associated with the expense. + * @param categoryId The identifier of the category associated with the expense. + * @param amount The amount of the expense. + * @param expenseDate The date of the expense. + * @param paymentType The payment type of the expense. + * @param categoryName The category name associated with the expense. */ public ExpenseUpdated { notNull(expenseId, "expenseId cannot be null."); @@ -41,5 +44,6 @@ public record ExpenseUpdated(UUID expenseId, notNull(amount, "amount cannot be null."); notNull(expenseDate, "expenseDate cannot be null."); notNull(paymentType, "paymentType cannot be null."); + notBlank(categoryName, "categoryName cannot be blank."); } } diff --git a/server/src/main/java/io/myfinbox/expense/application/DeleteExpenseService.java b/server/src/main/java/io/myfinbox/expense/application/DeleteExpenseService.java index 3a9018f..4c0560f 100644 --- a/server/src/main/java/io/myfinbox/expense/application/DeleteExpenseService.java +++ b/server/src/main/java/io/myfinbox/expense/application/DeleteExpenseService.java @@ -29,7 +29,7 @@ public Either delete(UUID expenseId) { return Either.left(Failure.ofNotFound(EXPENSE_NOT_FOUND_MESSAGE)); } - var possibleExpense = expenses.findById(new ExpenseIdentifier(expenseId)); + var possibleExpense = expenses.findByIdEagerCategory(new ExpenseIdentifier(expenseId)); if (possibleExpense.isEmpty()) { return Either.left(Failure.ofNotFound(EXPENSE_NOT_FOUND_MESSAGE)); } diff --git a/server/src/main/java/io/myfinbox/expense/domain/Expense.java b/server/src/main/java/io/myfinbox/expense/domain/Expense.java index e374c60..ee2da63 100644 --- a/server/src/main/java/io/myfinbox/expense/domain/Expense.java +++ b/server/src/main/java/io/myfinbox/expense/domain/Expense.java @@ -76,6 +76,7 @@ public Expense(AccountIdentifier account, .amount(this.amount) .expenseDate(this.expenseDate) .paymentType(this.paymentType) + .categoryName(category.getName()) .build()); } @@ -94,6 +95,7 @@ public void update(ExpenseBuilder builder) { .amount(this.amount) .expenseDate(this.expenseDate) .paymentType(this.paymentType) + .categoryName(builder.category.getName()) .build()); } @@ -113,6 +115,7 @@ public void delete() { .amount(this.amount) .expenseDate(this.expenseDate) .paymentType(this.paymentType) + .categoryName(getCategory().getName()) .build()); } diff --git a/server/src/main/java/io/myfinbox/expense/domain/Expenses.java b/server/src/main/java/io/myfinbox/expense/domain/Expenses.java index 6711bbf..627a273 100644 --- a/server/src/main/java/io/myfinbox/expense/domain/Expenses.java +++ b/server/src/main/java/io/myfinbox/expense/domain/Expenses.java @@ -1,6 +1,7 @@ package io.myfinbox.expense.domain; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.Optional; @@ -10,6 +11,13 @@ @Repository public interface Expenses extends JpaRepository { + @Query(value = """ + SELECT e FROM Expense e + JOIN FETCH e.category + WHERE e.id = :id + """) + Optional findByIdEagerCategory(ExpenseIdentifier id); + boolean existsByCategory(Category category); Optional findByIdAndAccount(ExpenseIdentifier id, AccountIdentifier account); diff --git a/server/src/main/java/io/myfinbox/spendingplan/adapter/messaging/ExpenseEventsListener.java b/server/src/main/java/io/myfinbox/spendingplan/adapter/messaging/ExpenseEventsListener.java index 7fa4aec..c238184 100644 --- a/server/src/main/java/io/myfinbox/spendingplan/adapter/messaging/ExpenseEventsListener.java +++ b/server/src/main/java/io/myfinbox/spendingplan/adapter/messaging/ExpenseEventsListener.java @@ -37,6 +37,7 @@ public void on(ExpenseCreated event) { .amount(event.amount()) .paymentType(event.paymentType()) .expenseDate(event.expenseDate()) + .categoryName(event.categoryName()) .build()); if (expenseRecord.isEmpty()) { @@ -61,6 +62,7 @@ public void on(ExpenseUpdated event) { .amount(event.amount()) .paymentType(event.paymentType()) .expenseDate(event.expenseDate()) + .categoryName(event.categoryName()) .build()); if (expenseRecord.isEmpty()) { @@ -85,6 +87,7 @@ public void on(ExpenseDeleted event) { .amount(event.amount()) .paymentType(event.paymentType()) .expenseDate(event.expenseDate()) + .categoryName(event.categoryName()) .build()); if (expenseRecord.isEmpty()) { diff --git a/server/src/main/java/io/myfinbox/spendingplan/application/ExpenseRecordTrackerService.java b/server/src/main/java/io/myfinbox/spendingplan/application/ExpenseRecordTrackerService.java index 4576c47..f7200c4 100644 --- a/server/src/main/java/io/myfinbox/spendingplan/application/ExpenseRecordTrackerService.java +++ b/server/src/main/java/io/myfinbox/spendingplan/application/ExpenseRecordTrackerService.java @@ -40,6 +40,7 @@ public List recordCreated(ExpenseModificationRecord createdRecord .amount(createdRecord.amount()) .paymentType(createdRecord.paymentType()) .expenseDate(createdRecord.expenseDate()) + .categoryName(createdRecord.categoryName()) .jarExpenseCategory(category) .build()) .toList(); diff --git a/server/src/main/java/io/myfinbox/spendingplan/application/ExpenseRecordTrackerUseCase.java b/server/src/main/java/io/myfinbox/spendingplan/application/ExpenseRecordTrackerUseCase.java index a4e654b..80125b1 100644 --- a/server/src/main/java/io/myfinbox/spendingplan/application/ExpenseRecordTrackerUseCase.java +++ b/server/src/main/java/io/myfinbox/spendingplan/application/ExpenseRecordTrackerUseCase.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.UUID; +import static io.myfinbox.shared.Guards.notBlank; import static io.myfinbox.shared.Guards.notNull; /** @@ -49,7 +50,8 @@ record ExpenseModificationRecord(UUID expenseId, UUID categoryId, MonetaryAmount amount, LocalDate expenseDate, - PaymentType paymentType) { + PaymentType paymentType, + String categoryName) { public ExpenseModificationRecord { // Validate non-null fields notNull(expenseId, "expenseId cannot be null."); @@ -58,6 +60,7 @@ record ExpenseModificationRecord(UUID expenseId, notNull(amount, "amount cannot be null."); notNull(expenseDate, "expenseDate cannot be null."); notNull(paymentType, "paymentType cannot be null."); + notBlank(categoryName, "categoryName cannot be blank."); } } } diff --git a/server/src/main/java/io/myfinbox/spendingplan/domain/ExpenseRecord.java b/server/src/main/java/io/myfinbox/spendingplan/domain/ExpenseRecord.java index 262671f..68c4263 100644 --- a/server/src/main/java/io/myfinbox/spendingplan/domain/ExpenseRecord.java +++ b/server/src/main/java/io/myfinbox/spendingplan/domain/ExpenseRecord.java @@ -12,6 +12,7 @@ import java.time.LocalDate; import java.util.UUID; +import static io.myfinbox.shared.Guards.notBlank; import static io.myfinbox.shared.Guards.notNull; import static lombok.AccessLevel.PACKAGE; @@ -47,6 +48,7 @@ public class ExpenseRecord { private PaymentType paymentType; private LocalDate expenseDate; + private String categoryName; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "jar_expense_category_id", referencedColumnName = "id", nullable = false) @@ -58,12 +60,14 @@ public ExpenseRecord(ExpenseIdentifier expenseId, MonetaryAmount amount, PaymentType paymentType, LocalDate expenseDate, + String categoryName, JarExpenseCategory jarExpenseCategory) { this.expenseId = notNull(expenseId, "expenseId cannot be null."); this.categoryId = notNull(categoryId, "categoryId cannot be null."); this.amount = notNull(amount, "amount cannot be null."); this.paymentType = notNull(paymentType, "paymentType cannot be null."); this.expenseDate = notNull(expenseDate, "expenseDate cannot be null."); + this.categoryName = notBlank(categoryName, "categoryName cannot be blank."); this.jarExpenseCategory = notNull(jarExpenseCategory, "jarExpenseCategory cannot be null."); this.creationTimestamp = Instant.now(); } diff --git a/server/src/main/resources/db/migration/V1_09__jar_expense_record_schema.sql b/server/src/main/resources/db/migration/V1_09__jar_expense_record_schema.sql index 4fd1f35..a603fa1 100644 --- a/server/src/main/resources/db/migration/V1_09__jar_expense_record_schema.sql +++ b/server/src/main/resources/db/migration/V1_09__jar_expense_record_schema.sql @@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS jar_expense_record amount DECIMAL(19, 4) NOT NULL, currency VARCHAR(3) NOT NULL, expense_date DATE NOT NULL, + category_name VARCHAR(100) NOT NULL, jar_expense_category_id BIGINT NOT NULL, FOREIGN KEY (jar_expense_category_id) REFERENCES spending_jar_expense_category (id) ) diff --git a/server/src/test/groovy/io/myfinbox/expense/application/DeleteExpenseServiceSpec.groovy b/server/src/test/groovy/io/myfinbox/expense/application/DeleteExpenseServiceSpec.groovy index ebdd7c8..27703ac 100644 --- a/server/src/test/groovy/io/myfinbox/expense/application/DeleteExpenseServiceSpec.groovy +++ b/server/src/test/groovy/io/myfinbox/expense/application/DeleteExpenseServiceSpec.groovy @@ -32,7 +32,7 @@ class DeleteExpenseServiceSpec extends Specification { def "should fail delete when expense not found"() { setup: 'repository mock behavior and interaction' - 1 * expenses.findById(_ as Expense.ExpenseIdentifier) >> Optional.empty() + 1 * expenses.findByIdEagerCategory(_ as Expense.ExpenseIdentifier) >> Optional.empty() when: 'expense failed to delete' def either = service.delete(UUID.randomUUID()) @@ -47,7 +47,7 @@ class DeleteExpenseServiceSpec extends Specification { def "should delete an expense"() { setup: 'repository mock behavior and interaction' def expense = newSampleExpense() - 1 * expenses.findById(_ as Expense.ExpenseIdentifier) >> Optional.of(expense) + 1 * expenses.findByIdEagerCategory(_ as Expense.ExpenseIdentifier) >> Optional.of(expense) when: 'expense is deleted' def either = service.delete(UUID.randomUUID()) diff --git a/server/src/test/groovy/io/myfinbox/spendingplan/DataSamples.groovy b/server/src/test/groovy/io/myfinbox/spendingplan/DataSamples.groovy index 7954f6b..dca2bef 100644 --- a/server/src/test/groovy/io/myfinbox/spendingplan/DataSamples.groovy +++ b/server/src/test/groovy/io/myfinbox/spendingplan/DataSamples.groovy @@ -131,30 +131,33 @@ class DataSamples { ] static EXPENSE_MODIFICATION_RECORD = [ - expenseId : expenseId, - accountId : accountId, - categoryId : jarCategoryId, - paymentType: "CASH", - amount : AMOUNT, - expenseDate: expenseDate, + expenseId : expenseId, + accountId : accountId, + categoryId : jarCategoryId, + paymentType : "CASH", + amount : AMOUNT, + expenseDate : expenseDate, + categoryName: categoryName, ] static EXPENSE_EVENT = [ - expenseId : expenseId, - accountId : accountId, - categoryId : jarCategoryId, - paymentType: "CASH", - amount : AMOUNT, - expenseDate: expenseDate, + expenseId : expenseId, + accountId : accountId, + categoryId : jarCategoryId, + paymentType : "CASH", + amount : AMOUNT, + expenseDate : expenseDate, + categoryName: categoryName, ] static EXPENSE_RECORD = [ - id : 1L, - expenseId : [id: expenseId], - categoryId : [id: jarCategoryId], - paymentType: "CASH", - amount : AMOUNT, - expenseDate: expenseDate, + id : 1L, + expenseId : [id: expenseId], + categoryId : [id: jarCategoryId], + paymentType : "CASH", + amount : AMOUNT, + categoryName: categoryName, + expenseDate : expenseDate, ] static JAR_EXPENSE_CATEGORY = [ diff --git a/server/src/test/groovy/io/myfinbox/spendingplan/adapter/messaging/ExpenseEventsListenerSpec.groovy b/server/src/test/groovy/io/myfinbox/spendingplan/adapter/messaging/ExpenseEventsListenerSpec.groovy index 88e9532..d02f029 100644 --- a/server/src/test/groovy/io/myfinbox/spendingplan/adapter/messaging/ExpenseEventsListenerSpec.groovy +++ b/server/src/test/groovy/io/myfinbox/spendingplan/adapter/messaging/ExpenseEventsListenerSpec.groovy @@ -73,6 +73,7 @@ class ExpenseEventsListenerSpec extends Specification { assert recordedExpense.amount == Money.of(amount, "EUR") assert recordedExpense.paymentType == PaymentType.CASH assert recordedExpense.expenseDate == LocalDate.parse(expenseDate) + assert recordedExpense.categoryName == categoryName } @Sql(['/spendingplan/messaging/create-complete-plan-structure.sql', '/spendingplan/messaging/create-expense-records.sql']) diff --git a/server/src/test/resources/spendingplan/messaging/create-expense-records.sql b/server/src/test/resources/spendingplan/messaging/create-expense-records.sql index 88d8195..5a549a4 100644 --- a/server/src/test/resources/spendingplan/messaging/create-expense-records.sql +++ b/server/src/test/resources/spendingplan/messaging/create-expense-records.sql @@ -5,6 +5,7 @@ INSERT INTO server.jar_expense_record(id, amount, currency, expense_date, + category_name, jar_expense_category_id) VALUES (1, '3b257779-a5db-4e87-9365-72c6f8d4977d', @@ -13,6 +14,7 @@ VALUES (1, 1000.0, 'EUR', '2024-03-23', + 'Clothing', 1), (2, '6bd32beb-5f79-409a-8c50-ecbd3593dc12', @@ -21,4 +23,5 @@ VALUES (1, 1000.0, 'EUR', '2024-03-23', + 'Clothing', 2);