merge main into branch

This commit is contained in:
2026-04-16 08:12:46 -06:00
49 changed files with 2187 additions and 400 deletions

View File

@@ -4,10 +4,12 @@ import com.petshop.backend.config.FlywayContextInitializer;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@EnableAsync
@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)
public class BackendApplication {
public static void main(String[] args) {

View File

@@ -10,7 +10,7 @@ public class ActivityLoggingFilterRegistrationConfig {
@Bean
public FilterRegistrationBean<ActivityLoggingFilter> activityLoggingFilterRegistration(ActivityLoggingFilter activityLoggingFilter) {
FilterRegistrationBean<ActivityLoggingFilter> registrationBean = new FilterRegistrationBean<>(activityLoggingFilter);
registrationBean.setEnabled(false);
registrationBean.setEnabled(true);
return registrationBean;
}
}

View File

@@ -0,0 +1,24 @@
package com.petshop.backend.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager("userAuthCache");
manager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(60, TimeUnit.SECONDS)
.maximumSize(1000));
return manager;
}
}

View File

@@ -0,0 +1,37 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.activity.ActivityLogResponse;
import com.petshop.backend.service.ActivityLogService;
import java.time.LocalDate;
import java.util.List;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/activity-logs")
@PreAuthorize("hasRole('ADMIN')")
public class ActivityLogController {
private final ActivityLogService activityLogService;
public ActivityLogController(ActivityLogService activityLogService) {
this.activityLogService = activityLogService;
}
@GetMapping
public ResponseEntity<List<ActivityLogResponse>> getActivityLogs(
@RequestParam(defaultValue = "2000") int limit,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String role,
@RequestParam(required = false) String search,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
int safeLimit = Math.min(Math.max(1, limit), 10000);
return ResponseEntity.ok(activityLogService.getLogs(safeLimit, storeId, role, search, startDate, endDate));
}
}

View File

@@ -15,6 +15,7 @@ import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.JwtUtil;
import com.petshop.backend.security.UserAuthCacheService;
import com.petshop.backend.service.ActivityLogService;
import com.petshop.backend.service.AvatarStorageService;
import com.petshop.backend.service.EmailService;
@@ -58,8 +59,9 @@ public class AuthController {
private final ActivityLogService activityLogService;
private final PasswordResetService passwordResetService;
private final EmailService emailService;
private final UserAuthCacheService userAuthCacheService;
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService, PasswordResetService passwordResetService, EmailService emailService) {
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService, PasswordResetService passwordResetService, EmailService emailService, UserAuthCacheService userAuthCacheService) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.jwtUtil = jwtUtil;
@@ -68,6 +70,7 @@ public class AuthController {
this.activityLogService = activityLogService;
this.passwordResetService = passwordResetService;
this.emailService = emailService;
this.userAuthCacheService = userAuthCacheService;
}
@PostMapping("/register")
@@ -263,6 +266,7 @@ public class AuthController {
error.put("message", "Username, email, or phone already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
userAuthCacheService.evict(updatedUser.getId());
return ResponseEntity.ok(toUserInfoResponse(updatedUser));
}

View File

@@ -3,6 +3,7 @@ package com.petshop.backend.controller;
import com.petshop.backend.dto.sale.SaleRequest;
import com.petshop.backend.dto.sale.SaleResponse;
import com.petshop.backend.service.SaleService;
import com.petshop.backend.util.AuthenticationHelper;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -21,6 +22,13 @@ public class SaleController {
this.saleService = saleService;
}
@GetMapping("/my")
@PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')")
public ResponseEntity<Page<SaleResponse>> getMyOrders(Pageable pageable) {
Long userId = AuthenticationHelper.getAuthenticatedUserId();
return ResponseEntity.ok(saleService.getAllSales(null, null, null, false, userId, pageable));
}
@GetMapping
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<Page<SaleResponse>> getAllSales(

View File

@@ -0,0 +1,126 @@
package com.petshop.backend.dto.activity;
import java.time.LocalDateTime;
public class ActivityLogResponse {
private Long logId;
private Long userId;
private String username;
private String fullName;
private String role;
private Long storeId;
private String storeName;
private String usernameSnapshot;
private String fullNameSnapshot;
private String roleSnapshot;
private String storeNameSnapshot;
private String activity;
private LocalDateTime logTimestamp;
public ActivityLogResponse() {
}
public Long getLogId() {
return logId;
}
public void setLogId(Long logId) {
this.logId = logId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
public String getUsernameSnapshot() {
return usernameSnapshot;
}
public void setUsernameSnapshot(String usernameSnapshot) {
this.usernameSnapshot = usernameSnapshot;
}
public String getFullNameSnapshot() {
return fullNameSnapshot;
}
public void setFullNameSnapshot(String fullNameSnapshot) {
this.fullNameSnapshot = fullNameSnapshot;
}
public String getRoleSnapshot() {
return roleSnapshot;
}
public void setRoleSnapshot(String roleSnapshot) {
this.roleSnapshot = roleSnapshot;
}
public String getStoreNameSnapshot() {
return storeNameSnapshot;
}
public void setStoreNameSnapshot(String storeNameSnapshot) {
this.storeNameSnapshot = storeNameSnapshot;
}
public String getActivity() {
return activity;
}
public void setActivity(String activity) {
this.activity = activity;
}
public LocalDateTime getLogTimestamp() {
return logTimestamp;
}
public void setLogTimestamp(LocalDateTime logTimestamp) {
this.logTimestamp = logTimestamp;
}
}

View File

@@ -1,9 +1,6 @@
package com.petshop.backend.dto.chat;
import jakarta.validation.constraints.NotBlank;
public class ConversationRequest {
@NotBlank(message = "Initial message is required")
private String message;
public ConversationRequest() {

View File

@@ -0,0 +1,148 @@
package com.petshop.backend.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.Immutable;
import java.time.LocalDateTime;
import java.util.Objects;
@Entity
@Immutable
@Table(name = "activityLog")
public class ActivityLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long logId;
@ManyToOne
@JoinColumn(name = "userId", nullable = false)
private User user;
@ManyToOne
@JoinColumn(name = "storeId")
private StoreLocation store;
@Column(length = 50)
private String usernameSnapshot;
@Column(length = 100)
private String fullNameSnapshot;
@Column(length = 20)
private String roleSnapshot;
@Column(length = 100)
private String storeNameSnapshot;
@Column(nullable = false, columnDefinition = "TEXT")
private String activity;
@Column(nullable = false)
private LocalDateTime logTimestamp = LocalDateTime.now();
public ActivityLog() {
}
public ActivityLog(Long logId, User user, String activity, LocalDateTime logTimestamp) {
this.logId = logId;
this.user = user;
this.activity = activity;
this.logTimestamp = logTimestamp;
}
public Long getLogId() {
return logId;
}
public void setLogId(Long logId) {
this.logId = logId;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public StoreLocation getStore() {
return store;
}
public void setStore(StoreLocation store) {
this.store = store;
}
public String getUsernameSnapshot() {
return usernameSnapshot;
}
public void setUsernameSnapshot(String usernameSnapshot) {
this.usernameSnapshot = usernameSnapshot;
}
public String getFullNameSnapshot() {
return fullNameSnapshot;
}
public void setFullNameSnapshot(String fullNameSnapshot) {
this.fullNameSnapshot = fullNameSnapshot;
}
public String getRoleSnapshot() {
return roleSnapshot;
}
public void setRoleSnapshot(String roleSnapshot) {
this.roleSnapshot = roleSnapshot;
}
public String getStoreNameSnapshot() {
return storeNameSnapshot;
}
public void setStoreNameSnapshot(String storeNameSnapshot) {
this.storeNameSnapshot = storeNameSnapshot;
}
public String getActivity() {
return activity;
}
public void setActivity(String activity) {
this.activity = activity;
}
public LocalDateTime getLogTimestamp() {
return logTimestamp;
}
public void setLogTimestamp(LocalDateTime logTimestamp) {
this.logTimestamp = logTimestamp;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ActivityLog that = (ActivityLog) o;
return Objects.equals(logId, that.logId);
}
@Override
public int hashCode() {
return Objects.hash(logId);
}
@Override
public String toString() {
return "ActivityLog{" +
"logId=" + logId +
", user=" + user +
", activity='" + activity + '\'' +
", logTimestamp=" + logTimestamp +
'}';
}
}

View File

@@ -0,0 +1,18 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.ActivityLog;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ActivityLogRepository extends JpaRepository<ActivityLog, Long>, JpaSpecificationExecutor<ActivityLog> {
boolean existsByUser_Id(Long userId);
@Query("select a from ActivityLog a order by a.logTimestamp desc, a.logId desc")
List<ActivityLog> findRecent(Pageable pageable);
}

View File

@@ -41,11 +41,11 @@ public interface PetRepository extends JpaRepository<Pet, Long> {
Optional<Pet> findByIdAndOwner_Id(Long id, Long ownerId);
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Pet p WHERE p.petId = :id")
@Query("SELECT p FROM Pet p WHERE p.id = :id")
Optional<Pet> findByIdForUpdate(@Param("id") Long id);
@Query("SELECT p FROM Pet p WHERE " +
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
@Query("SELECT p FROM Pet p LEFT JOIN p.owner o WHERE " +
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(o.firstName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(o.lastName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(CONCAT(COALESCE(o.firstName, ''), ' ', COALESCE(o.lastName, ''))) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:breed IS NULL OR LOWER(COALESCE(p.petBreed, '')) = LOWER(:breed)) AND " +
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status)) AND " +

View File

@@ -2,7 +2,6 @@ package com.petshop.backend.security;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ApiErrorResponder;
import com.petshop.backend.repository.UserRepository;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
@@ -16,16 +15,18 @@ import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Date;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final UserAuthCacheService userAuthCacheService;
private final ApiErrorResponder apiErrorResponder;
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserRepository userRepository, ApiErrorResponder apiErrorResponder) {
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserAuthCacheService userAuthCacheService, ApiErrorResponder apiErrorResponder) {
this.jwtUtil = jwtUtil;
this.userRepository = userRepository;
this.userAuthCacheService = userAuthCacheService;
this.apiErrorResponder = apiErrorResponder;
}
@@ -44,30 +45,44 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
jwt = authHeader.substring(7);
Long userId;
String username;
String roleStr;
Integer jwtTokenVersion;
try {
userId = jwtUtil.extractUserId(jwt);
username = jwtUtil.extractUsername(jwt);
roleStr = jwtUtil.extractRole(jwt);
jwtTokenVersion = jwtUtil.extractTokenVersion(jwt);
} catch (JwtException | IllegalArgumentException ex) {
writeUnauthorized(request, response, "Invalid or expired token", ex);
return;
}
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
User user = userRepository.findById(userId).orElse(null);
if (user == null || user.getActive() == null || !user.getActive()) {
writeUnauthorized(request, response, "User account is inactive", null);
return;
}
if (!jwtUtil.validateToken(jwt, user)) {
if (jwtUtil.extractExpiration(jwt).before(new Date())) {
writeUnauthorized(request, response, "Invalid or expired token", null);
return;
}
AppPrincipal principal = new AppPrincipal(
user.getId(),
user.getUsername(),
user.getRole(),
user.getTokenVersion()
);
UserAuthCacheService.UserAuthData authData = userAuthCacheService.loadAuthData(userId);
if (authData == null || !Boolean.TRUE.equals(authData.active())) {
writeUnauthorized(request, response, "User account is inactive", null);
return;
}
if (!authData.tokenVersion().equals(jwtTokenVersion)) {
writeUnauthorized(request, response, "Invalid or expired token", null);
return;
}
User.Role role;
try {
role = User.Role.valueOf(roleStr);
} catch (IllegalArgumentException ex) {
writeUnauthorized(request, response, "Invalid or expired token", ex);
return;
}
AppPrincipal principal = new AppPrincipal(userId, username, role, jwtTokenVersion);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
principal,
null,

View File

@@ -1,5 +1,6 @@
package com.petshop.backend.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
@@ -21,6 +22,8 @@ import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import com.petshop.backend.config.ActivityLoggingFilter;
import java.util.List;
@@ -30,6 +33,9 @@ import java.util.List;
@EnableMethodSecurity
public class SecurityConfig {
@Value("${app.allowed-origins}")
private String allowedOriginsRaw;
private final JwtAuthenticationFilter jwtAuthFilter;
private final RateLimitFilter rateLimitFilter;
private final UserDetailsService userDetailsService;
@@ -101,13 +107,13 @@ public class SecurityConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("http://localhost:*", "http://127.0.0.1:*"));
config.setAllowedOriginPatterns(Arrays.asList(allowedOriginsRaw.split(",")));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}

View File

@@ -0,0 +1,29 @@
package com.petshop.backend.security;
import com.petshop.backend.repository.UserRepository;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserAuthCacheService {
private final UserRepository userRepository;
public UserAuthCacheService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public record UserAuthData(Boolean active, Integer tokenVersion) {}
@Cacheable(value = "userAuthCache", key = "#userId")
public UserAuthData loadAuthData(Long userId) {
return userRepository.findById(userId)
.map(u -> new UserAuthData(u.getActive(), u.getTokenVersion()))
.orElse(null);
}
@CacheEvict(value = "userAuthCache", key = "#userId")
public void evict(Long userId) {
}
}

View File

@@ -1,38 +1,65 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.activity.ActivityLogResponse;
import com.petshop.backend.entity.ActivityLog;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.ActivityLogRepository;
import com.petshop.backend.repository.UserRepository;
import jakarta.persistence.criteria.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
@Service
public class ActivityLogService {
private static final Logger log = LoggerFactory.getLogger("activity");
private static final Logger log = LoggerFactory.getLogger(ActivityLogService.class);
private final ActivityLogRepository activityLogRepository;
private final UserRepository userRepository;
public ActivityLogService(UserRepository userRepository) {
public ActivityLogService(ActivityLogRepository activityLogRepository, UserRepository userRepository) {
this.activityLogRepository = activityLogRepository;
this.userRepository = userRepository;
}
@Transactional
public void record(Long userId, String activity) {
if (userId == null || activity == null || activity.isBlank()) {
return;
}
try {
User user = userRepository.findById(userId).orElse(null);
if (user == null) {
User managedUser = userRepository.findById(userId).orElse(null);
if (managedUser == null) {
return;
}
StoreLocation store = user.getPrimaryStore();
String role = user.getRole() != null ? user.getRole().name() : "UNKNOWN";
String storeName = store != null ? store.getStoreName() : "no store";
log.info("{} | {} | {} | {}", role, user.getUsername(), storeName, activity.trim());
StoreLocation store = managedUser.getPrimaryStore();
ActivityLog entry = new ActivityLog();
entry.setUser(managedUser);
entry.setStore(store);
entry.setUsernameSnapshot(managedUser.getUsername());
entry.setFullNameSnapshot(resolveFullName(managedUser));
entry.setRoleSnapshot(managedUser.getRole() != null ? managedUser.getRole().name() : null);
entry.setStoreNameSnapshot(store != null ? store.getStoreName() : null);
entry.setActivity(activity.trim());
activityLogRepository.save(entry);
log.info("[ACTIVITY] {} | {} | {} | {}",
entry.getRoleSnapshot(),
entry.getUsernameSnapshot(),
entry.getStoreNameSnapshot() != null ? entry.getStoreNameSnapshot() : "no store",
entry.getActivity());
} catch (Exception ex) {
log.warn("Failed to record activity", ex);
log.warn("Failed to persist activity log", ex);
}
}
@@ -42,4 +69,106 @@ public class ActivityLogService {
}
record(user.getId(), activity);
}
@Transactional(readOnly = true)
public List<ActivityLogResponse> getLogs(int limit, Long storeId, String role, String search, LocalDate startDate, LocalDate endDate) {
Specification<ActivityLog> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (storeId != null) {
predicates.add(cb.equal(root.get("store").get("storeId"), storeId));
}
if (role != null && !role.isBlank()) {
predicates.add(cb.equal(root.get("roleSnapshot"), role));
}
if (search != null && !search.isBlank()) {
String pattern = "%" + search.toLowerCase() + "%";
Predicate searchPredicate = cb.or(
cb.like(cb.lower(root.get("activity")), pattern),
cb.like(cb.lower(root.get("fullNameSnapshot")), pattern),
cb.like(cb.lower(root.get("usernameSnapshot")), pattern)
);
predicates.add(searchPredicate);
}
if (startDate != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("logTimestamp"), startDate.atStartOfDay()));
}
if (endDate != null) {
predicates.add(cb.lessThan(root.get("logTimestamp"), endDate.plusDays(1).atStartOfDay()));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
PageRequest pageRequest = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "logTimestamp", "logId"));
return activityLogRepository.findAll(spec, pageRequest).stream()
.map(this::toResponse)
.toList();
}
@Transactional(readOnly = true)
public List<ActivityLogResponse> getLogs(int limit, Long storeId, String role, String search) {
return getLogs(limit, storeId, role, search, null, null);
}
@Transactional(readOnly = true)
public List<ActivityLogResponse> getLogs(int limit) {
return getLogs(limit, null, null, null, null, null);
}
private ActivityLogResponse toResponse(ActivityLog entry) {
ActivityLogResponse response = new ActivityLogResponse();
response.setLogId(entry.getLogId());
if (entry.getUser() != null) {
response.setUserId(entry.getUser().getId());
response.setUsername(firstNonBlank(entry.getUsernameSnapshot(), entry.getUser().getUsername()));
response.setFullName(firstNonBlank(entry.getFullNameSnapshot(), resolveFullName(entry.getUser())));
response.setRole(firstNonBlank(entry.getRoleSnapshot(), entry.getUser().getRole() != null ? entry.getUser().getRole().name() : null));
}
StoreLocation store = entry.getStore();
if (store != null) {
response.setStoreId(store.getStoreId());
response.setStoreName(firstNonBlank(entry.getStoreNameSnapshot(), store.getStoreName()));
}
response.setUsernameSnapshot(entry.getUsernameSnapshot());
response.setFullNameSnapshot(entry.getFullNameSnapshot());
response.setRoleSnapshot(entry.getRoleSnapshot());
response.setStoreNameSnapshot(entry.getStoreNameSnapshot());
response.setActivity(entry.getActivity());
response.setLogTimestamp(entry.getLogTimestamp());
return response;
}
private String resolveFullName(User user) {
if (user == null) {
return null;
}
if (user.getFullName() != null && !user.getFullName().isBlank()) {
return user.getFullName();
}
String first = user.getFirstName();
String last = user.getLastName();
if (first == null || first.isBlank()) {
return last;
}
if (last == null || last.isBlank()) {
return first;
}
return first.trim() + " " + last.trim();
}
private String firstNonBlank(String preferred, String fallback) {
if (preferred != null && !preferred.isBlank()) {
return preferred;
}
return fallback;
}
}

View File

@@ -127,8 +127,6 @@ public class AppointmentService {
}
}
validateSpeciesServiceCompatibility(pet, service);
validateStoreAccess(store.getStoreId(), authenticatedUser);
validatePetServiceCompatibility(pet, service);
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
@@ -398,32 +396,6 @@ public class AppointmentService {
}
}
private void validateSpeciesServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) {
if (pet == null || service == null) return;
String species = pet.getPetSpecies();
if (species == null) return;
String serviceName = service.getServiceName().toLowerCase();
switch (species.toLowerCase()) {
case "bird":
if (!serviceName.contains("wing clipping") && !serviceName.contains("beak and nail")) {
throw new IllegalArgumentException(
"Service '" + service.getServiceName() + "' is not available for birds. " +
"Allowed services: Wing Clipping, Beak and Nail Care.");
}
break;
case "fish":
if (!serviceName.contains("aquarium health")) {
throw new IllegalArgumentException(
"Service '" + service.getServiceName() + "' is not available for fish. " +
"Allowed service: Aquarium Health Check.");
}
break;
default:
break;
}
}
private void validateStoreAccess(Long requestedStoreId, User user) {
if (user.getRole() != User.Role.STAFF) {
return;

View File

@@ -59,14 +59,20 @@ public class ChatService {
conversation.setMode(Conversation.ConversationMode.AUTOMATED);
conversation = conversationRepository.save(conversation);
Message message = new Message();
message.setConversationId(conversation.getId());
message.setSenderId(userId);
message.setContent(request.getMessage());
message.setIsRead(false);
messageRepository.save(message);
User botUser = getBotUser();
String firstName = user.getFirstName();
String greeting = (firstName != null && !firstName.isBlank())
? "Hi " + firstName + "! I'm Leon's Pet Assistant. Ask me anything about pet care, adoption advice, or your pets."
: "Hi! I'm Leon's Pet Assistant. Ask me anything about pet care, adoption advice, or your pets.";
return ConversationResponse.fromEntity(conversation, request.getMessage(), userId);
Message greetingMsg = new Message();
greetingMsg.setConversationId(conversation.getId());
greetingMsg.setSenderId(botUser.getId());
greetingMsg.setContent(greeting);
greetingMsg.setIsRead(false);
messageRepository.save(greetingMsg);
return ConversationResponse.fromEntity(conversation, greeting, botUser.getId());
}
public List<ConversationResponse> getConversations(Long userId, User.Role role, boolean mine) {

View File

@@ -5,8 +5,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.petshop.backend.entity.Conversation;
import com.petshop.backend.entity.Message;
import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.MessageRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,7 +33,7 @@ public class OpenRouterAiService {
@Value("${openrouter.api-key:}")
private String apiKey;
@Value("${openrouter.model:openai/gpt-oss-120b:free}")
@Value("${openrouter.model:google/gemma-4-31b-it:free}")
private String model;
private final String openRouterUrl = "https://openrouter.ai/api/v1/chat/completions";
@@ -39,6 +41,7 @@ public class OpenRouterAiService {
private final ChatRealtimeService chatRealtimeService;
private final MessageRepository messageRepository;
private final UserRepository userRepository;
private final PetRepository petRepository;
private final ObjectMapper objectMapper;
private final HttpClient httpClient;
@@ -46,12 +49,14 @@ public class OpenRouterAiService {
ChatService chatService,
ChatRealtimeService chatRealtimeService,
MessageRepository messageRepository,
UserRepository userRepository
UserRepository userRepository,
PetRepository petRepository
) {
this.chatService = chatService;
this.chatRealtimeService = chatRealtimeService;
this.messageRepository = messageRepository;
this.userRepository = userRepository;
this.petRepository = petRepository;
this.objectMapper = JsonMapper.builder().findAndAddModules().build();
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
@@ -117,10 +122,15 @@ public class OpenRouterAiService {
return;
}
User customer = userRepository.findById(conversation.getCustomerId()).orElse(null);
List<Pet> customerPets = customer != null
? petRepository.findAllByOwner_IdOrderByPetNameAsc(customer.getId())
: List.of();
List<Map<String, String>> messages = new ArrayList<>();
messages.add(Map.of(
"role", "system",
"content", "You are a helpful pet shop assistant. Provide concise and friendly answers. Do not output markdown, just plain text."
"content", buildSystemPrompt(customer, customerPets)
));
for (Message message : history) {
@@ -177,6 +187,43 @@ public class OpenRouterAiService {
}
}
private String buildSystemPrompt(User customer, List<Pet> pets) {
StringBuilder sb = new StringBuilder();
sb.append("You are Leon's Pet Assistant, a helpful AI for Leon's Pet Store. ");
sb.append("Be concise, friendly, and focused on pet care, adoption, products, and appointments. ");
sb.append("Do not output markdown, just plain text.\n\n");
if (customer != null) {
sb.append("Customer profile:\n");
sb.append("- Name: ").append(customer.getFirstName()).append(" ").append(customer.getLastName()).append("\n");
if (customer.getLoyaltyPoints() != null) {
sb.append("- Loyalty points: ").append(customer.getLoyaltyPoints()).append("\n");
}
if (customer.getPrimaryStore() != null && customer.getPrimaryStore().getStoreName() != null) {
sb.append("- Preferred store: ").append(customer.getPrimaryStore().getStoreName()).append("\n");
}
sb.append("\n");
}
if (pets != null && !pets.isEmpty()) {
sb.append("Their registered pets:\n");
for (Pet pet : pets) {
sb.append("- ").append(pet.getPetName()).append(" (").append(pet.getPetSpecies());
if (pet.getPetBreed() != null && !pet.getPetBreed().isBlank()) {
sb.append(", ").append(pet.getPetBreed());
}
if (pet.getPetAge() != null) {
sb.append(", ").append(pet.getPetAge()).append(" yr");
}
sb.append(")\n");
}
} else {
sb.append("They have no pets registered yet.\n");
}
return sb.toString();
}
private String resolveRole(Message message, Long botUserId) {
if (message.getSenderId() != null && message.getSenderId().equals(botUserId)) {
return "assistant";

View File

@@ -24,7 +24,7 @@ public class OpenRouterService {
@Value("${openrouter.api-key:}")
private String apiKey;
@Value("${openrouter.model:meta-llama/llama-3.3-70b-instruct:free}")
@Value("${openrouter.model:google/gemma-4-31b-it:free}")
private String model;
private final ObjectMapper objectMapper = new ObjectMapper();

View File

@@ -7,6 +7,7 @@ import com.petshop.backend.entity.User;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.repository.PasswordResetTokenRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.UserAuthCacheService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -29,16 +30,19 @@ public class PasswordResetService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final EmailService emailService;
private final UserAuthCacheService userAuthCacheService;
private final SecureRandom secureRandom = new SecureRandom();
public PasswordResetService(PasswordResetTokenRepository passwordResetTokenRepository,
UserRepository userRepository,
PasswordEncoder passwordEncoder,
EmailService emailService) {
EmailService emailService,
UserAuthCacheService userAuthCacheService) {
this.passwordResetTokenRepository = passwordResetTokenRepository;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.emailService = emailService;
this.userAuthCacheService = userAuthCacheService;
}
@Transactional
@@ -97,6 +101,7 @@ public class PasswordResetService {
user.setPassword(passwordEncoder.encode(newPassword));
user.setTokenVersion(user.getTokenVersion() + 1);
userRepository.save(user);
userAuthCacheService.evict(user.getId());
token.setUsedAt(now);
passwordResetTokenRepository.save(token);

View File

@@ -8,6 +8,7 @@ import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.UserAuthCacheService;
import com.petshop.backend.util.AuthenticationHelper;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -33,11 +34,13 @@ public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final StoreRepository storeRepository;
private final UserAuthCacheService userAuthCacheService;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, StoreRepository storeRepository) {
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, StoreRepository storeRepository, UserAuthCacheService userAuthCacheService) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.storeRepository = storeRepository;
this.userAuthCacheService = userAuthCacheService;
}
public Page<UserResponse> getAllUsers(String query, String role, Pageable pageable) {
@@ -147,6 +150,7 @@ public class UserService {
}
user = userRepository.save(user);
userAuthCacheService.evict(user.getId());
return mapToResponse(user);
}

View File

@@ -18,6 +18,10 @@ spring:
username: ${SPRING_DATASOURCE_USERNAME:petshop}
password: ${SPRING_DATASOURCE_PASSWORD:petshop}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
sql:
init:
@@ -46,14 +50,16 @@ server:
springdoc:
api-docs:
path: /v3/api-docs
enabled: ${SWAGGER_ENABLED:false}
swagger-ui:
path: /swagger-ui
enabled: ${SWAGGER_ENABLED:false}
app:
upload:
base-dir: ${UPLOAD_BASE_DIR:uploads}
frontend-url: ${FRONTEND_URL:http://localhost:3000}
allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000,http://localhost:3001,http://127.0.0.1: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}
azure:
storage:
@@ -82,9 +88,3 @@ logging:
com.petshop: ${LOG_LEVEL:INFO}
org.springframework.security: ${LOG_LEVEL_SECURITY:WARN}
org.springdoc.core.events.SpringDocAppInitializer: ERROR
jackson:
serialization:
write-dates-as-timestamps: false
deserialization:
fail-on-unknown-properties: false

View File

@@ -0,0 +1,14 @@
DELETE FROM service_species WHERE serviceId = 2 AND species = 'Bird';
INSERT INTO service_species (serviceId, species) VALUES
(1, 'Guinea Pig'),
(1, 'Hamster'),
(1, 'Other'),
(2, 'Reptile'),
(2, 'Other'),
(3, 'Reptile'),
(3, 'Other'),
(4, 'Reptile'),
(4, 'Other'),
(5, 'Reptile'),
(5, 'Other');

View File

@@ -0,0 +1,90 @@
INSERT INTO activityLog (userId, storeId, usernameSnapshot, fullNameSnapshot, roleSnapshot, storeNameSnapshot, activity, logTimestamp) VALUES
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 08:02:11'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new pet | POST /api/v1/pets → 201', '2026-01-15 08:15:44'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new product | POST /api/v1/products → 201', '2026-01-15 08:31:07'),
(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 09:00:00'),
(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 09:04:22'),
(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-01-15 10:12:33'),
(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-01-15 10:18:05'),
(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-01-15 10:25:50'),
(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Updated appointment #12 | PUT /api/v1/appointments/12 → 200', '2026-01-16 11:05:30'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-17 07:58:44'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated pet #8 | PUT /api/v1/pets/8 → 200', '2026-01-17 08:10:19'),
(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-20 09:01:55'),
(16, NULL,'alex.brown', 'Alex Brown', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-01-20 14:30:22'),
(16, NULL,'alex.brown', 'Alex Brown', 'CUSTOMER', NULL, 'Submitted an adoption request | POST /api/v1/adoptions/request → 201', '2026-01-20 14:45:08'),
(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-22 08:55:00'),
(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Updated pet #14 | PUT /api/v1/pets/14 → 200', '2026-01-22 09:20:17'),
(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-01-25 08:00:00'),
(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Created a new service | POST /api/v1/services → 201', '2026-01-25 08:22:41'),
(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Created a new employee | POST /api/v1/employees → 201', '2026-01-25 09:05:14'),
(17, NULL,'alex.clark', 'Alex Clark', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-01-28 18:30:00'),
(17, NULL,'alex.clark', 'Alex Clark', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-01-28 18:35:12'),
(17, NULL,'alex.clark', 'Alex Clark', 'CUSTOMER', NULL, 'Updated their profile | PUT /api/v1/auth/me → 200', '2026-01-28 18:40:55'),
(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-03 09:00:00'),
(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-02-03 10:15:38'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-05 07:50:00'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Deleted pet #3 | DELETE /api/v1/pets/3 → 200', '2026-02-05 08:05:33'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated product #22 | PUT /api/v1/products/22 → 200', '2026-02-05 08:30:44'),
(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-02-07 12:00:00'),
(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-02-07 12:08:17'),
(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Applied a coupon to cart | POST /api/v1/cart/apply-coupon → 200', '2026-02-07 12:12:05'),
(18, NULL,'alex.wilson', 'Alex Wilson', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-02-07 12:20:30'),
(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-10 09:00:00'),
(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Created a new appointment | POST /api/v1/appointments → 201', '2026-02-10 09:30:22'),
(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Updated appointment #25 | PUT /api/v1/appointments/25 → 200', '2026-02-10 11:45:09'),
(19, NULL,'alex.martinez', 'Alex Martinez', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-02-14 15:00:00'),
(19, NULL,'alex.martinez', 'Alex Martinez', 'CUSTOMER', NULL, 'Started a new chat conversation | POST /api/v1/chat/conversations → 201','2026-02-14 15:05:44'),
(19, NULL,'alex.martinez', 'Alex Martinez', 'CUSTOMER', NULL, 'Sent a chat message | POST /api/v1/chat/conversations/5/messages → 201', '2026-02-14 15:08:22'),
(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-17 09:00:00'),
(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Uploaded image for pet #31 | POST /api/v1/pets/31/image → 200', '2026-02-17 09:25:11'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-02-20 08:00:00'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new pet | POST /api/v1/pets → 201', '2026-02-20 08:20:35'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated user #18 | PUT /api/v1/users/18 → 200', '2026-02-20 09:10:00'),
(20, NULL,'alex.anderson', 'Alex Anderson', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-02-22 19:00:00'),
(20, NULL,'alex.anderson', 'Alex Anderson', 'CUSTOMER', NULL, 'Submitted an adoption request | POST /api/v1/adoptions/request → 201', '2026-02-22 19:15:40'),
(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-01 09:00:00'),
(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-01 10:30:15'),
(7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Updated appointment #33 | PUT /api/v1/appointments/33 → 200', '2026-03-01 14:05:22'),
(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-03-05 11:00:00'),
(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-03-05 11:10:30'),
(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Started checkout | POST /api/v1/cart/checkout → 200', '2026-03-05 11:18:44'),
(21, NULL,'alex.taylor', 'Alex Taylor', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-05 11:22:17'),
(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-08 08:00:00'),
(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Deleted multiple pets | POST /api/v1/pets/bulk-delete → 200', '2026-03-08 08:30:00'),
(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-10 09:00:00'),
(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-10 10:45:33'),
(22, NULL,'alex.parker', 'Alex Parker', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-03-12 16:00:00'),
(22, NULL,'alex.parker', 'Alex Parker', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-03-12 16:05:22'),
(22, NULL,'alex.parker', 'Alex Parker', 'CUSTOMER', NULL, 'Updated their profile | PUT /api/v1/auth/me → 200', '2026-03-12 16:20:00'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-15 07:55:00'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new product | POST /api/v1/products → 201', '2026-03-15 08:10:45'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated product #35 | PUT /api/v1/products/35 → 200', '2026-03-15 08:40:12'),
(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-18 09:00:00'),
(5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Updated appointment #45 | PUT /api/v1/appointments/45 → 200', '2026-03-18 09:35:08'),
(23, NULL,'alex.evans', 'Alex Evans', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-03-20 20:00:00'),
(23, NULL,'alex.evans', 'Alex Evans', 'CUSTOMER', NULL, 'Started a new chat conversation | POST /api/v1/chat/conversations → 201','2026-03-20 20:05:30'),
(23, NULL,'alex.evans', 'Alex Evans', 'CUSTOMER', NULL, 'Sent a chat message | POST /api/v1/chat/conversations/9/messages → 201', '2026-03-20 20:08:11'),
(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-24 09:00:00'),
(11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Created a new appointment | POST /api/v1/appointments → 201', '2026-03-24 09:20:44'),
(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-03-27 09:00:00'),
(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Updated appointment #52 | PUT /api/v1/appointments/52 → 200', '2026-03-27 10:10:19'),
(8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-03-27 11:30:00'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-01 08:00:00'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Created a new pet | POST /api/v1/pets → 201', '2026-04-01 08:15:00'),
(1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated user #25 | PUT /api/v1/users/25 → 200', '2026-04-01 09:00:22'),
(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-05 10:00:00'),
(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-04-05 10:15:00'),
(15, NULL,'customer', 'Test Customer', 'CUSTOMER', NULL, 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-04-05 10:25:44'),
(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-07 09:00:00'),
(3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Updated pet #47 | PUT /api/v1/pets/47 → 200', '2026-04-07 09:40:55'),
(24, NULL,'alex.scott', 'Alex Scott', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-09 17:00:00'),
(24, NULL,'alex.scott', 'Alex Scott', 'CUSTOMER', NULL, 'Submitted an adoption request | POST /api/v1/adoptions/request → 201', '2026-04-09 17:20:33'),
(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-12 08:00:00'),
(2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Created a new service | POST /api/v1/services → 201', '2026-04-12 08:25:18'),
(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Logged in | POST /api/v1/auth/login → 200', '2026-04-14 09:00:00'),
(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Completed a purchase | POST /api/v1/cart/checkout/complete → 200', '2026-04-14 10:55:30'),
(4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Updated appointment #60 | PUT /api/v1/appointments/60 → 200', '2026-04-14 14:20:00'),
(25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Logged in | POST /api/v1/auth/login → 200', '2026-04-15 11:00:00'),
(25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Added an item to cart | POST /api/v1/cart/add → 200', '2026-04-15 11:10:22'),
(25, NULL,'alex.adams', 'Alex Adams', 'CUSTOMER', NULL, 'Sent a message to the AI assistant | POST /api/v1/ai-chat/message → 200','2026-04-15 11:30:00');

View File

@@ -0,0 +1,51 @@
INSERT IGNORE INTO sale (saleId, saleDate, totalAmount, paymentMethod, employeeId, storeId, customerId, isRefund, originalSaleId, channel, cartId, couponId, subtotalAmount, couponDiscountAmount, employeeDiscountAmount, pointsEarned) VALUES
(134, '2026-04-10 09:15:00', 57.67, 'Cash', 4, 1, 16, 0, NULL, 'IN_STORE', NULL, NULL, 57.67, 0.00, 0.00, 5),
(135, '2026-04-10 11:30:00', 143.55, 'Card', 8, 2, 17, 0, NULL, 'ONLINE', NULL, NULL, 143.55, 0.00, 0.00, 14),
(136, '2026-04-10 14:45:00', 50.09, 'Cash', 12, 3, 18, 0, NULL, 'IN_STORE', NULL, NULL, 50.09, 0.00, 0.00, 5),
(137, '2026-04-11 10:00:00', 114.48, 'Card', 5, 1, 19, 0, NULL, 'ONLINE', NULL, NULL, 114.48, 0.00, 0.00, 11),
(138, '2026-04-11 13:20:00', 93.55, 'Cash', 9, 2, 20, 0, NULL, 'IN_STORE', NULL, NULL, 93.55, 0.00, 0.00, 9),
(139, '2026-04-12 09:45:00', 100.71, 'Card', 13, 3, 21, 0, NULL, 'ONLINE', NULL, NULL, 100.71, 0.00, 0.00, 10),
(140, '2026-04-12 11:00:00', 51.07, 'Cash', 6, 1, 22, 0, NULL, 'IN_STORE', NULL, NULL, 51.07, 0.00, 0.00, 5),
(141, '2026-04-12 15:30:00', 139.66, 'Card', 7, 2, 23, 0, NULL, 'ONLINE', NULL, NULL, 139.66, 0.00, 0.00, 13),
(142, '2026-04-13 09:00:00', 73.98, 'Cash', 14, 3, 24, 0, NULL, 'IN_STORE', NULL, NULL, 73.98, 0.00, 0.00, 7),
(143, '2026-04-13 12:15:00', 134.76, 'Card', 4, 1, 25, 0, NULL, 'ONLINE', NULL, NULL, 134.76, 0.00, 0.00, 13),
(144, '2026-04-14 10:30:00', 80.40, 'Cash', 10, 2, 26, 0, NULL, 'IN_STORE', NULL, NULL, 80.40, 0.00, 0.00, 8),
(145, '2026-04-14 14:00:00', 125.90, 'Card', 11, 3, 27, 0, NULL, 'ONLINE', NULL, NULL, 125.90, 0.00, 0.00, 12),
(146, '2026-04-15 10:45:00', 80.62, 'Cash', 5, 1, 28, 0, NULL, 'IN_STORE', NULL, NULL, 80.62, 0.00, 0.00, 8),
(147, '2026-04-15 13:00:00', 141.28, 'Card', 8, 2, 29, 0, NULL, 'ONLINE', NULL, NULL, 141.28, 0.00, 0.00, 14),
(148, '2026-04-16 09:30:00', 97.85, 'Cash', 12, 3, 30, 0, NULL, 'IN_STORE', NULL, NULL, 97.85, 0.00, 0.00, 9),
(149, '2026-04-16 11:45:00', 89.36, 'Card', 6, 1, 31, 0, NULL, 'ONLINE', NULL, NULL, 89.36, 0.00, 0.00, 8);
INSERT IGNORE INTO saleItem (saleItemId, saleId, prodId, quantity, unitPrice) VALUES
(264, 134, 1, 2, 25.09),
(265, 134, 11, 1, 7.49),
(266, 135, 7, 2, 57.62),
(267, 135, 25, 1, 28.31),
(268, 136, 3, 1, 35.93),
(269, 136, 14, 1, 14.16),
(270, 137, 15, 3, 16.38),
(271, 137, 26, 2, 32.67),
(272, 138, 8, 1, 63.05),
(273, 138, 22, 2, 15.25),
(274, 139, 20, 2, 27.49),
(275, 139, 29, 1, 45.73),
(276, 140, 4, 1, 41.36),
(277, 140, 12, 1, 9.71),
(278, 141, 34, 2, 59.42),
(279, 141, 17, 1, 20.82),
(280, 142, 6, 1, 52.20),
(281, 142, 21, 2, 10.89),
(282, 143, 37, 1, 97.56),
(283, 143, 16, 2, 18.60),
(284, 144, 9, 1, 68.47),
(285, 144, 13, 1, 11.93),
(286, 145, 19, 3, 25.27),
(287, 145, 30, 1, 50.09),
(288, 146, 2, 2, 30.51),
(289, 146, 23, 1, 19.60),
(290, 147, 35, 1, 72.13),
(291, 147, 18, 3, 23.05),
(292, 148, 10, 1, 73.89),
(293, 148, 24, 1, 23.96),
(294, 149, 31, 2, 21.29),
(295, 149, 5, 1, 46.78);