diff --git a/apps/api/src/main/java/com/upkeep/infrastructure/adapter/in/rest/common/exception/GlobalExceptionMapper.java b/apps/api/src/main/java/com/upkeep/infrastructure/adapter/in/rest/common/exception/GlobalExceptionMapper.java index 5b54cf62..4c678796 100644 --- a/apps/api/src/main/java/com/upkeep/infrastructure/adapter/in/rest/common/exception/GlobalExceptionMapper.java +++ b/apps/api/src/main/java/com/upkeep/infrastructure/adapter/in/rest/common/exception/GlobalExceptionMapper.java @@ -1,6 +1,7 @@ package com.upkeep.infrastructure.adapter.in.rest.common.exception; import com.upkeep.domain.exception.AlreadyMemberException; +import com.upkeep.domain.exception.BudgetAlreadyExistsException; import com.upkeep.domain.exception.CompanyNotFoundException; import com.upkeep.domain.exception.CompanySlugAlreadyExistsException; import com.upkeep.domain.exception.CustomerAlreadyExistsException; @@ -151,6 +152,13 @@ private Response handleDomainException(DomainException exception, String traceId )) .build(); + case BudgetAlreadyExistsException e -> Response + .status(409) + .entity(ApiResponse.error( + ApiError.of("BUDGET_ALREADY_EXISTS", e.getMessage(), traceId) + )) + .build(); + default -> Response .status(422) .entity(ApiResponse.error( diff --git a/apps/api/src/test/java/com/upkeep/application/usecase/SetCompanyBudgetUseCaseImplTest.java b/apps/api/src/test/java/com/upkeep/application/usecase/SetCompanyBudgetUseCaseImplTest.java index 5c3030de..3581f257 100644 --- a/apps/api/src/test/java/com/upkeep/application/usecase/SetCompanyBudgetUseCaseImplTest.java +++ b/apps/api/src/test/java/com/upkeep/application/usecase/SetCompanyBudgetUseCaseImplTest.java @@ -6,6 +6,7 @@ import com.upkeep.application.port.out.audit.AuditEventRepository; import com.upkeep.application.port.out.budget.BudgetRepository; import com.upkeep.application.port.out.membership.MembershipRepository; +import com.upkeep.domain.exception.BudgetAlreadyExistsException; import com.upkeep.domain.exception.MembershipNotFoundException; import com.upkeep.domain.exception.UnauthorizedOperationException; import com.upkeep.domain.model.audit.AuditEvent; @@ -19,6 +20,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.time.Instant; import java.util.Optional; import java.util.UUID; @@ -137,6 +139,33 @@ void shouldSetBudgetWithDifferentCurrencies() { assertEquals(100000L, result.amountCents()); } + @Test + @DisplayName("should throw exception when budget already exists for current month") + void shouldThrowExceptionWhenBudgetAlreadyExists() { + String companyId = UUID.randomUUID().toString(); + String userId = UUID.randomUUID().toString(); + + Membership ownerMembership = createOwnerMembership(userId, companyId); + when(membershipRepository.findByCustomerIdAndCompanyId( + any(CustomerId.class), + any(CompanyId.class) + )).thenReturn(Optional.of(ownerMembership)); + + // Mock that a budget already exists for the current month + Budget existingBudget = mock(Budget.class); + when(budgetRepository.findByCompanyIdAndEffectiveFrom( + any(CompanyId.class), + any(Instant.class) + )).thenReturn(Optional.of(existingBudget)); + + SetBudgetCommand command = new SetBudgetCommand(companyId, userId, 50000L, Currency.EUR); + + assertThrows(BudgetAlreadyExistsException.class, () -> useCase.execute(command)); + + verify(budgetRepository, never()).save(any(Budget.class)); + verify(auditEventRepository, never()).save(any(AuditEvent.class)); + } + private Membership createOwnerMembership(String userId, String companyId) { return Membership.create( CustomerId.from(userId), diff --git a/apps/api/src/test/java/com/upkeep/infrastructure/adapter/in/rest/budget/BudgetResourceTest.java b/apps/api/src/test/java/com/upkeep/infrastructure/adapter/in/rest/budget/BudgetResourceTest.java index 865dad6f..1f652a9b 100644 --- a/apps/api/src/test/java/com/upkeep/infrastructure/adapter/in/rest/budget/BudgetResourceTest.java +++ b/apps/api/src/test/java/com/upkeep/infrastructure/adapter/in/rest/budget/BudgetResourceTest.java @@ -313,4 +313,41 @@ void shouldSetBudgetWithDifferentCurrencies() { .body("data.currency", equalTo(currency)); } } + + @Test + @DisplayName("should reject duplicate budget for same month") + void shouldRejectDuplicateBudgetForSameMonth() { + String uniqueId = UUID.randomUUID().toString().substring(0, 8); + String email = "duplicate-budget-" + uniqueId + "@example.com"; + String token = createUserAndGetToken(email); + String companyId = createCompany(token, "Duplicate Budget Company", "duplicate-budget-" + uniqueId); + + String requestBody = """ + { + "amountCents": 50000, + "currency": "EUR" + } + """; + + // First budget should succeed + given() + .contentType(ContentType.JSON) + .cookie("access_token", token) + .body(requestBody) + .when() + .post("/api/companies/" + companyId + "/budget") + .then() + .statusCode(201); + + // Second budget for the same month should fail + given() + .contentType(ContentType.JSON) + .cookie("access_token", token) + .body(requestBody) + .when() + .post("/api/companies/" + companyId + "/budget") + .then() + .statusCode(409) + .body("error.code", equalTo("BUDGET_ALREADY_EXISTS")); + } }