externalize business constants

This commit is contained in:
2026-04-17 14:43:00 -06:00
parent 80df6116ab
commit d198fb3d42
7 changed files with 58 additions and 29 deletions

View File

@@ -1,13 +1,16 @@
package com.petshop.backend; package com.petshop.backend;
import com.petshop.backend.config.BusinessProperties;
import com.petshop.backend.config.FlywayContextInitializer; import com.petshop.backend.config.FlywayContextInitializer;
import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@EnableConfigurationProperties(BusinessProperties.class)
@EnableScheduling @EnableScheduling
@EnableAsync @EnableAsync
@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) @EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)

View File

@@ -0,0 +1,25 @@
package com.petshop.backend.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.math.BigDecimal;
import java.time.LocalTime;
@ConfigurationProperties(prefix = "petshop.business")
public record BusinessProperties(
LocalTime openTime,
LocalTime closeTime,
int slotIntervalMinutes,
long maxImageSizeBytes,
BigDecimal employeeDiscountPercent,
int loyaltyPointsPerDollar
) {
public BusinessProperties {
if (openTime == null) openTime = LocalTime.of(9, 0);
if (closeTime == null) closeTime = LocalTime.of(17, 0);
if (slotIntervalMinutes <= 0) slotIntervalMinutes = 30;
if (maxImageSizeBytes <= 0) maxImageSizeBytes = 5 * 1024 * 1024;
if (employeeDiscountPercent == null) employeeDiscountPercent = new BigDecimal("0.10");
if (loyaltyPointsPerDollar <= 0) loyaltyPointsPerDollar = 20;
}
}

View File

@@ -19,6 +19,7 @@ import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.ServiceRepository;
import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository; import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.config.BusinessProperties;
import com.petshop.backend.util.AuthenticationHelper; import com.petshop.backend.util.AuthenticationHelper;
import com.petshop.backend.util.StringUtils; import com.petshop.backend.util.StringUtils;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
@@ -47,8 +48,9 @@ public class AppointmentService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final AdoptionRepository adoptionRepository; private final AdoptionRepository adoptionRepository;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
private final BusinessProperties businessProperties;
public AppointmentService(AppointmentRepository appointmentRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, AdoptionRepository adoptionRepository, ApplicationEventPublisher eventPublisher) { public AppointmentService(AppointmentRepository appointmentRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, AdoptionRepository adoptionRepository, ApplicationEventPublisher eventPublisher, BusinessProperties businessProperties) {
this.appointmentRepository = appointmentRepository; this.appointmentRepository = appointmentRepository;
this.serviceRepository = serviceRepository; this.serviceRepository = serviceRepository;
this.petRepository = petRepository; this.petRepository = petRepository;
@@ -56,6 +58,7 @@ public class AppointmentService {
this.userRepository = userRepository; this.userRepository = userRepository;
this.adoptionRepository = adoptionRepository; this.adoptionRepository = adoptionRepository;
this.eventPublisher = eventPublisher; this.eventPublisher = eventPublisher;
this.businessProperties = businessProperties;
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -240,8 +243,8 @@ public class AppointmentService {
.collect(Collectors.groupingBy(a -> a.getEmployee().getId())); .collect(Collectors.groupingBy(a -> a.getEmployee().getId()));
List<String> availableSlots = new ArrayList<>(); List<String> availableSlots = new ArrayList<>();
LocalTime startTime = LocalTime.of(9, 0); LocalTime startTime = businessProperties.openTime();
LocalTime endTime = LocalTime.of(17, 0); LocalTime endTime = businessProperties.closeTime();
LocalTime latestStart = endTime.minusMinutes(service.getServiceDuration()); LocalTime latestStart = endTime.minusMinutes(service.getServiceDuration());
LocalTime currentTime = startTime; LocalTime currentTime = startTime;
@@ -255,7 +258,7 @@ public class AppointmentService {
if (anyEmployeeAvailable) { if (anyEmployeeAvailable) {
availableSlots.add(currentTime.toString()); availableSlots.add(currentTime.toString());
} }
currentTime = currentTime.plusMinutes(30); currentTime = currentTime.plusMinutes(businessProperties.slotIntervalMinutes());
} }
return availableSlots; return availableSlots;

View File

@@ -7,7 +7,7 @@ import com.petshop.backend.entity.*;
import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.*; import com.petshop.backend.repository.*;
import com.petshop.backend.util.BusinessConstants; import com.petshop.backend.config.BusinessProperties;
import com.stripe.Stripe; import com.stripe.Stripe;
import com.stripe.exception.StripeException; import com.stripe.exception.StripeException;
import com.stripe.model.PaymentIntent; import com.stripe.model.PaymentIntent;
@@ -25,8 +25,6 @@ import java.util.List;
@Service @Service
public class CartService { public class CartService {
private static final int LOYALTY_POINTS_PER_DOLLAR = BusinessConstants.LOYALTY_POINTS_PER_DOLLAR;
private final CartRepository cartRepository; private final CartRepository cartRepository;
private final CartItemRepository cartItemRepository; private final CartItemRepository cartItemRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
@@ -36,6 +34,7 @@ public class CartService {
private final CouponService couponService; private final CouponService couponService;
private final SaleRepository saleRepository; private final SaleRepository saleRepository;
private final SaleService saleService; private final SaleService saleService;
private final BusinessProperties businessProperties;
@Value("${stripe.secret-key:}") @Value("${stripe.secret-key:}")
private String stripeSecretKey; private String stripeSecretKey;
@@ -48,7 +47,8 @@ public class CartService {
CouponRepository couponRepository, CouponRepository couponRepository,
CouponService couponService, CouponService couponService,
SaleRepository saleRepository, SaleRepository saleRepository,
SaleService saleService) { SaleService saleService,
BusinessProperties businessProperties) {
this.cartRepository = cartRepository; this.cartRepository = cartRepository;
this.cartItemRepository = cartItemRepository; this.cartItemRepository = cartItemRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
@@ -58,6 +58,7 @@ public class CartService {
this.couponService = couponService; this.couponService = couponService;
this.saleRepository = saleRepository; this.saleRepository = saleRepository;
this.saleService = saleService; this.saleService = saleService;
this.businessProperties = businessProperties;
} }
@PostConstruct @PostConstruct
@@ -473,7 +474,7 @@ public class CartService {
} }
int availablePoints = user.getLoyaltyPoints() != null ? user.getLoyaltyPoints() : 0; int availablePoints = user.getLoyaltyPoints() != null ? user.getLoyaltyPoints() : 0;
int wholeDollars = availablePoints / LOYALTY_POINTS_PER_DOLLAR; int wholeDollars = availablePoints / businessProperties.loyaltyPointsPerDollar();
if (wholeDollars <= 0) { if (wholeDollars <= 0) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }

View File

@@ -8,7 +8,7 @@ import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.event.SaleReceiptEvent; import com.petshop.backend.event.SaleReceiptEvent;
import com.petshop.backend.repository.*; import com.petshop.backend.repository.*;
import com.petshop.backend.util.AuthenticationHelper; import com.petshop.backend.util.AuthenticationHelper;
import com.petshop.backend.util.BusinessConstants; import com.petshop.backend.config.BusinessProperties;
import com.petshop.backend.util.StringUtils; import com.petshop.backend.util.StringUtils;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@@ -25,9 +25,6 @@ import java.util.List;
@Service @Service
public class SaleService { public class SaleService {
private static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = BusinessConstants.EMPLOYEE_DISCOUNT_PERCENT;
private static final int LOYALTY_POINTS_PER_DOLLAR = BusinessConstants.LOYALTY_POINTS_PER_DOLLAR;
private final SaleRepository saleRepository; private final SaleRepository saleRepository;
private final ProductRepository productRepository; private final ProductRepository productRepository;
private final StoreRepository storeRepository; private final StoreRepository storeRepository;
@@ -37,8 +34,9 @@ public class SaleService {
private final CouponService couponService; private final CouponService couponService;
private final CartRepository cartRepository; private final CartRepository cartRepository;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
private final BusinessProperties businessProperties;
public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CouponService couponService, CartRepository cartRepository, ApplicationEventPublisher eventPublisher) { public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CouponService couponService, CartRepository cartRepository, ApplicationEventPublisher eventPublisher, BusinessProperties businessProperties) {
this.saleRepository = saleRepository; this.saleRepository = saleRepository;
this.productRepository = productRepository; this.productRepository = productRepository;
this.storeRepository = storeRepository; this.storeRepository = storeRepository;
@@ -48,6 +46,7 @@ public class SaleService {
this.couponService = couponService; this.couponService = couponService;
this.cartRepository = cartRepository; this.cartRepository = cartRepository;
this.eventPublisher = eventPublisher; this.eventPublisher = eventPublisher;
this.businessProperties = businessProperties;
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -251,7 +250,7 @@ public class SaleService {
int pointsDeducted; int pointsDeducted;
if (request.getPointsUsed() != null && request.getPointsUsed() > 0) { if (request.getPointsUsed() != null && request.getPointsUsed() > 0) {
loyaltyDiscount = BigDecimal.valueOf(request.getPointsUsed()) loyaltyDiscount = BigDecimal.valueOf(request.getPointsUsed())
.divide(BigDecimal.valueOf(LOYALTY_POINTS_PER_DOLLAR), 2, RoundingMode.HALF_UP) .divide(BigDecimal.valueOf(businessProperties.loyaltyPointsPerDollar()), 2, RoundingMode.HALF_UP)
.min(remainingAfterDiscounts.max(BigDecimal.ZERO)) .min(remainingAfterDiscounts.max(BigDecimal.ZERO))
.setScale(2, RoundingMode.HALF_UP); .setScale(2, RoundingMode.HALF_UP);
pointsDeducted = request.getPointsUsed(); pointsDeducted = request.getPointsUsed();
@@ -292,7 +291,7 @@ public class SaleService {
} }
if (customer.getRole() == User.Role.STAFF || customer.getRole() == User.Role.ADMIN) { if (customer.getRole() == User.Role.STAFF || customer.getRole() == User.Role.ADMIN) {
return remainingAmount.multiply(EMPLOYEE_DISCOUNT_PERCENT).setScale(2, RoundingMode.HALF_UP); return remainingAmount.multiply(businessProperties.employeeDiscountPercent()).setScale(2, RoundingMode.HALF_UP);
} }
return BigDecimal.ZERO; return BigDecimal.ZERO;
@@ -304,7 +303,7 @@ public class SaleService {
} }
int availablePoints = customer.getLoyaltyPoints() != null ? customer.getLoyaltyPoints() : 0; int availablePoints = customer.getLoyaltyPoints() != null ? customer.getLoyaltyPoints() : 0;
int wholeDollars = availablePoints / LOYALTY_POINTS_PER_DOLLAR; int wholeDollars = availablePoints / businessProperties.loyaltyPointsPerDollar();
if (wholeDollars <= 0) { if (wholeDollars <= 0) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
@@ -319,7 +318,7 @@ public class SaleService {
if (loyaltyDiscount == null || loyaltyDiscount.compareTo(BigDecimal.ZERO) <= 0) { if (loyaltyDiscount == null || loyaltyDiscount.compareTo(BigDecimal.ZERO) <= 0) {
return 0; return 0;
} }
return loyaltyDiscount.setScale(0, RoundingMode.DOWN).intValue() * LOYALTY_POINTS_PER_DOLLAR; return loyaltyDiscount.setScale(0, RoundingMode.DOWN).intValue() * businessProperties.loyaltyPointsPerDollar();
} }
private User resolveWebsiteSaleEmployee(Long storeId) { private User resolveWebsiteSaleEmployee(Long storeId) {

View File

@@ -1,11 +0,0 @@
package com.petshop.backend.util;
import java.math.BigDecimal;
public final class BusinessConstants {
private BusinessConstants() {}
public static final int LOYALTY_POINTS_PER_DOLLAR = 20;
public static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = new BigDecimal("0.10");
}

View File

@@ -61,6 +61,15 @@ app:
frontend-url: ${FRONTEND_URL:http://localhost:3000} frontend-url: ${FRONTEND_URL:http://localhost:3000}
allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000,http://localhost:3001,http://127.0.0.1:3000,https://petshop-web.nicepond-c7280126.westus2.azurecontainerapps.io} allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000,http://localhost:3001,http://127.0.0.1:3000,https://petshop-web.nicepond-c7280126.westus2.azurecontainerapps.io}
petshop:
business:
open-time: "09:00"
close-time: "17:00"
slot-interval-minutes: 30
max-image-size-bytes: 5242880
employee-discount-percent: 0.10
loyalty-points-per-dollar: 20
azure: azure:
storage: storage:
connection-string: ${AZURE_STORAGE_CONNECTION_STRING:} connection-string: ${AZURE_STORAGE_CONNECTION_STRING:}