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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.upkeep.application.port.in.budget;

import com.upkeep.domain.model.budget.Currency;

public interface UpdateCompanyBudgetUseCase {

UpdateBudgetResult execute(UpdateBudgetCommand command);

record UpdateBudgetCommand(
String companyId,
String actorUserId,
long newAmountCents,
Currency currency
) {}

record UpdateBudgetResult(
String budgetId,
long amountCents,
String currency,
boolean isLowerThanAllocations,
long currentAllocationsCents
) {}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.upkeep.application.usecase;

import com.upkeep.application.port.in.budget.UpdateCompanyBudgetUseCase;
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.BudgetNotFoundException;
import com.upkeep.domain.exception.MembershipNotFoundException;
import com.upkeep.domain.exception.UnauthorizedOperationException;
import com.upkeep.domain.model.audit.AuditEvent;
import com.upkeep.domain.model.budget.Budget;
import com.upkeep.domain.model.budget.Money;
import com.upkeep.domain.model.company.CompanyId;
import com.upkeep.domain.model.customer.CustomerId;
import com.upkeep.domain.model.membership.Membership;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

@ApplicationScoped
public class UpdateCompanyBudgetUseCaseImpl implements UpdateCompanyBudgetUseCase {

private final BudgetRepository budgetRepository;
private final AuditEventRepository auditEventRepository;
private final MembershipRepository membershipRepository;

@Inject
public UpdateCompanyBudgetUseCaseImpl(BudgetRepository budgetRepository,
AuditEventRepository auditEventRepository,
MembershipRepository membershipRepository) {
this.budgetRepository = budgetRepository;
this.auditEventRepository = auditEventRepository;
this.membershipRepository = membershipRepository;
}

@Override
@Transactional
public UpdateBudgetResult execute(UpdateBudgetCommand command) {
CompanyId companyId = CompanyId.from(command.companyId());
CustomerId actorId = CustomerId.from(command.actorUserId());

Membership membership = membershipRepository.findByCustomerIdAndCompanyId(actorId, companyId)
.orElseThrow(() -> new MembershipNotFoundException(command.actorUserId(), command.companyId()));

if (!membership.isOwner()) {
throw new UnauthorizedOperationException("Only owners can update the company budget");
}

Budget budget = budgetRepository.findByCompanyId(companyId)
.orElseThrow(() -> new BudgetNotFoundException(command.companyId()));

long previousAmountCents = budget.getAmount().amountCents();
long currentAllocationsCents = calculateCurrentAllocations(companyId);

Money newAmount = new Money(command.newAmountCents(), command.currency());
budget.updateAmount(newAmount);
budgetRepository.save(budget);

AuditEvent event = AuditEvent.budgetUpdated(companyId, actorId, budget, previousAmountCents);
auditEventRepository.save(event);

boolean isLowerThanAllocations = command.newAmountCents() < currentAllocationsCents;

return new UpdateBudgetResult(
budget.getId().toString(),
newAmount.amountCents(),
newAmount.currency().name(),
isLowerThanAllocations,
currentAllocationsCents
);
}

private long calculateCurrentAllocations(CompanyId companyId) {
// TODO: Implement when allocations are available (Story 4.x)
// For now, return 0 as allocations are not yet implemented
return 0L;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.upkeep.domain.exception;

/**
* Thrown when attempting to update a budget that does not exist.
*/
public class BudgetNotFoundException extends DomainException {

private final String companyId;

public BudgetNotFoundException(String companyId) {
super("No budget found for this company");
this.companyId = companyId;
}

public String getCompanyId() {
return companyId;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import com.upkeep.application.port.in.budget.SetCompanyBudgetUseCase;
import com.upkeep.application.port.in.budget.SetCompanyBudgetUseCase.SetBudgetCommand;
import com.upkeep.application.port.in.budget.SetCompanyBudgetUseCase.SetBudgetResult;
import com.upkeep.application.port.in.budget.UpdateCompanyBudgetUseCase;
import com.upkeep.application.port.in.budget.UpdateCompanyBudgetUseCase.UpdateBudgetCommand;
import com.upkeep.application.port.in.budget.UpdateCompanyBudgetUseCase.UpdateBudgetResult;
import com.upkeep.application.port.out.auth.TokenService;
import com.upkeep.application.port.out.auth.TokenService.TokenClaims;
import com.upkeep.domain.model.budget.Currency;
Expand All @@ -14,6 +17,7 @@
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PATCH;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
Expand All @@ -29,13 +33,16 @@ public class BudgetResource {
private static final String ACCESS_TOKEN_COOKIE = "access_token";

private final SetCompanyBudgetUseCase setCompanyBudgetUseCase;
private final UpdateCompanyBudgetUseCase updateCompanyBudgetUseCase;
private final GetBudgetSummaryUseCase getBudgetSummaryUseCase;
private final TokenService tokenService;

public BudgetResource(SetCompanyBudgetUseCase setCompanyBudgetUseCase,
UpdateCompanyBudgetUseCase updateCompanyBudgetUseCase,
GetBudgetSummaryUseCase getBudgetSummaryUseCase,
TokenService tokenService) {
this.setCompanyBudgetUseCase = setCompanyBudgetUseCase;
this.updateCompanyBudgetUseCase = updateCompanyBudgetUseCase;
this.getBudgetSummaryUseCase = getBudgetSummaryUseCase;
this.tokenService = tokenService;
}
Expand Down Expand Up @@ -91,6 +98,38 @@ public Response setBudget(@CookieParam(ACCESS_TOKEN_COOKIE) String accessToken,
.build();
}

@PATCH
public Response updateBudget(@CookieParam(ACCESS_TOKEN_COOKIE) String accessToken,
@PathParam("companyId") String companyId,
@Valid UpdateBudgetRequest request) {
TokenClaims claims = validateToken(accessToken);

if (claims == null) {
return unauthorizedResponse();
}

Currency currency = Currency.valueOf(request.currency());

UpdateBudgetResult result = updateCompanyBudgetUseCase.execute(
new UpdateBudgetCommand(
companyId,
claims.userId(),
request.amountCents(),
currency
)
);

UpdateBudgetResponse response = new UpdateBudgetResponse(
result.budgetId(),
result.amountCents(),
result.currency(),
result.isLowerThanAllocations(),
result.currentAllocationsCents()
);

return Response.ok(ApiResponse.success(response)).build();
}

private TokenClaims validateToken(String accessToken) {
if (accessToken == null || accessToken.isBlank()) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.upkeep.infrastructure.adapter.in.rest.budget;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;

public record UpdateBudgetRequest(
@Min(value = 100, message = "Budget must be at least 100 cents (1.00)")
long amountCents,

@NotBlank(message = "Currency is required")
@Pattern(regexp = "EUR|USD|GBP", message = "Currency must be EUR, USD, or GBP")
String currency
) {
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.upkeep.infrastructure.adapter.in.rest.budget;

public record UpdateBudgetResponse(
String budgetId,
long amountCents,
String currency,
boolean isLowerThanAllocations,
long currentAllocationsCents
) {
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ public class BudgetJpaRepository implements BudgetRepository, PanacheRepositoryB
@Override
public void save(Budget budget) {
BudgetEntity entity = BudgetMapper.toEntity(budget);
persist(entity);
BudgetEntity existingEntity = findById(budget.getId().value());

if (existingEntity != null) {
existingEntity.amountCents = entity.amountCents;
existingEntity.currency = entity.currency;
existingEntity.updatedAt = entity.updatedAt;
} else {
persist(entity);
}
}

@Override
Expand Down
Loading