Merge branch 'main' into AttachmentsToChat

This commit is contained in:
Alex
2026-04-14 00:57:16 -06:00
333 changed files with 1524 additions and 369 deletions

1
.gitignore vendored
View File

@@ -2,5 +2,4 @@
.local/ .local/
commit-patches/ commit-patches/
temp_photos/ temp_photos/
uploads/
.env .env

5
backend/.gitignore vendored
View File

@@ -46,7 +46,10 @@ build/
### Project Specific ### ### Project Specific ###
src/test/ src/test/
tmp/ tmp/
uploads/ uploads/*
!uploads/avatars/
!uploads/pets/
!uploads/products/
### Temp and backup files ### ### Temp and backup files ###
*.backup *.backup

View File

@@ -9,5 +9,6 @@ RUN mvn -q -DskipTests package
FROM eclipse-temurin:25-jre FROM eclipse-temurin:25-jre
WORKDIR /app WORKDIR /app
COPY --from=build /app/target/*.jar app.jar COPY --from=build /app/target/*.jar app.jar
COPY uploads ./uploads
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"] ENTRYPOINT ["java","-jar","app.jar"]

View File

@@ -151,6 +151,12 @@
<configuration> <configuration>
<mainClass>com.petshop.backend.DevStackApplication</mainClass> <mainClass>com.petshop.backend.DevStackApplication</mainClass>
<classpathScope>runtime</classpathScope> <classpathScope>runtime</classpathScope>
<systemProperties>
<systemProperty>
<key>UPLOAD_BASE_DIR</key>
<value>${project.basedir}/uploads</value>
</systemProperty>
</systemProperties>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>

View File

@@ -1,11 +1,15 @@
package com.petshop.backend.controller; package com.petshop.backend.controller;
import com.petshop.backend.dto.auth.AvatarUploadResponse; import com.petshop.backend.dto.auth.AvatarUploadResponse;
import com.petshop.backend.dto.auth.ForgotPasswordRequest;
import com.petshop.backend.dto.auth.ForgotPasswordResponse;
import com.petshop.backend.dto.auth.LoginRequest; import com.petshop.backend.dto.auth.LoginRequest;
import com.petshop.backend.dto.auth.LoginResponse; import com.petshop.backend.dto.auth.LoginResponse;
import com.petshop.backend.dto.auth.ProfileUpdateRequest; import com.petshop.backend.dto.auth.ProfileUpdateRequest;
import com.petshop.backend.dto.auth.RegisterRequest; import com.petshop.backend.dto.auth.RegisterRequest;
import com.petshop.backend.dto.auth.RegisterResponse; import com.petshop.backend.dto.auth.RegisterResponse;
import com.petshop.backend.dto.auth.ResetPasswordRequest;
import com.petshop.backend.dto.auth.ResetPasswordResponse;
import com.petshop.backend.dto.auth.UserInfoResponse; import com.petshop.backend.dto.auth.UserInfoResponse;
import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User; import com.petshop.backend.entity.User;
@@ -13,6 +17,7 @@ import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.JwtUtil; import com.petshop.backend.security.JwtUtil;
import com.petshop.backend.service.ActivityLogService; import com.petshop.backend.service.ActivityLogService;
import com.petshop.backend.service.AvatarStorageService; import com.petshop.backend.service.AvatarStorageService;
import com.petshop.backend.service.PasswordResetService;
import com.petshop.backend.util.AuthenticationHelper; import com.petshop.backend.util.AuthenticationHelper;
import com.petshop.backend.util.PhoneUtils; import com.petshop.backend.util.PhoneUtils;
import jakarta.validation.Valid; import jakarta.validation.Valid;
@@ -49,14 +54,16 @@ public class AuthController {
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final AvatarStorageService avatarStorageService; private final AvatarStorageService avatarStorageService;
private final ActivityLogService activityLogService; private final ActivityLogService activityLogService;
private final PasswordResetService passwordResetService;
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService) { public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService, PasswordResetService passwordResetService) {
this.authenticationManager = authenticationManager; this.authenticationManager = authenticationManager;
this.userRepository = userRepository; this.userRepository = userRepository;
this.jwtUtil = jwtUtil; this.jwtUtil = jwtUtil;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.avatarStorageService = avatarStorageService; this.avatarStorageService = avatarStorageService;
this.activityLogService = activityLogService; this.activityLogService = activityLogService;
this.passwordResetService = passwordResetService;
} }
@PostMapping("/register") @PostMapping("/register")
@@ -153,6 +160,16 @@ public class AuthController {
} }
} }
@PostMapping("/forgot-password")
public ResponseEntity<ForgotPasswordResponse> forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) {
return ResponseEntity.ok(passwordResetService.createResetToken(request.getUsernameOrEmail()));
}
@PostMapping("/reset-password")
public ResponseEntity<ResetPasswordResponse> resetPassword(@Valid @RequestBody ResetPasswordRequest request) {
return ResponseEntity.ok(passwordResetService.resetPassword(request.getToken(), request.getNewPassword()));
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
@GetMapping("/me") @GetMapping("/me")
public ResponseEntity<UserInfoResponse> getCurrentUser() { public ResponseEntity<UserInfoResponse> getCurrentUser() {

View File

@@ -0,0 +1,17 @@
package com.petshop.backend.dto.auth;
import jakarta.validation.constraints.NotBlank;
public class ForgotPasswordRequest {
@NotBlank(message = "Username or email is required")
private String usernameOrEmail;
public String getUsernameOrEmail() {
return usernameOrEmail;
}
public void setUsernameOrEmail(String usernameOrEmail) {
this.usernameOrEmail = usernameOrEmail;
}
}

View File

@@ -0,0 +1,20 @@
package com.petshop.backend.dto.auth;
public class ForgotPasswordResponse {
private final String message;
private final String resetToken;
public ForgotPasswordResponse(String message, String resetToken) {
this.message = message;
this.resetToken = resetToken;
}
public String getMessage() {
return message;
}
public String getResetToken() {
return resetToken;
}
}

View File

@@ -0,0 +1,30 @@
package com.petshop.backend.dto.auth;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class ResetPasswordRequest {
@NotBlank(message = "Reset token is required")
private String token;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String newPassword;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getNewPassword() {
return newPassword;
}
public void setNewPassword(String newPassword) {
this.newPassword = newPassword;
}
}

View File

@@ -0,0 +1,14 @@
package com.petshop.backend.dto.auth;
public class ResetPasswordResponse {
private final String message;
public ResetPasswordResponse(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}

View File

@@ -11,8 +11,11 @@ public class CartResponse {
private List<CartItemResponse> items; private List<CartItemResponse> items;
private BigDecimal subtotalAmount; private BigDecimal subtotalAmount;
private BigDecimal discountAmount; private BigDecimal discountAmount;
private BigDecimal pointsDiscountAmount;
private BigDecimal totalAmount; private BigDecimal totalAmount;
private String couponCode; private String couponCode;
private Boolean pointsApplied;
private Integer availableLoyaltyPoints;
public CartResponse() { public CartResponse() {
} }
@@ -40,4 +43,14 @@ public class CartResponse {
public String getCouponCode() { return couponCode; } public String getCouponCode() { return couponCode; }
public void setCouponCode(String couponCode) { this.couponCode = couponCode; } public void setCouponCode(String couponCode) { this.couponCode = couponCode; }
public BigDecimal getPointsDiscountAmount() { return pointsDiscountAmount; }
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) { this.pointsDiscountAmount = pointsDiscountAmount; }
public Boolean getPointsApplied() { return pointsApplied; }
public void setPointsApplied(Boolean pointsApplied) { this.pointsApplied = pointsApplied; }
public Integer getAvailableLoyaltyPoints() { return availableLoyaltyPoints; }
public void setAvailableLoyaltyPoints(Integer availableLoyaltyPoints) { this.availableLoyaltyPoints = availableLoyaltyPoints; }
} }

View File

@@ -5,7 +5,6 @@ import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.math.BigDecimal;
public class SaleRequest { public class SaleRequest {
@NotNull(message = "Store ID is required") @NotNull(message = "Store ID is required")
@@ -29,9 +28,6 @@ public class SaleRequest {
private Long cartId; private Long cartId;
private Integer pointsUsed;
private BigDecimal pointsDiscountAmount;
public Long getStoreId() { public Long getStoreId() {
return storeId; return storeId;
@@ -105,21 +101,6 @@ public class SaleRequest {
this.cartId = cartId; this.cartId = cartId;
} }
public Integer getPointsUsed() {
return pointsUsed;
}
public void setPointsUsed(Integer pointsUsed) {
this.pointsUsed = pointsUsed;
}
public BigDecimal getPointsDiscountAmount() {
return pointsDiscountAmount;
}
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
this.pointsDiscountAmount = pointsDiscountAmount;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {

View File

@@ -18,9 +18,8 @@ public class SaleResponse {
private BigDecimal subtotalAmount; private BigDecimal subtotalAmount;
private BigDecimal couponDiscountAmount; private BigDecimal couponDiscountAmount;
private BigDecimal employeeDiscountAmount; private BigDecimal employeeDiscountAmount;
private BigDecimal loyaltyDiscountAmount;
private Integer pointsEarned; private Integer pointsEarned;
private Integer pointsUsed;
private BigDecimal pointsDiscountAmount;
private String channel; private String channel;
private Long couponId; private Long couponId;
private Long cartId; private Long cartId;
@@ -129,6 +128,15 @@ public class SaleResponse {
this.employeeDiscountAmount = employeeDiscountAmount; this.employeeDiscountAmount = employeeDiscountAmount;
} }
public BigDecimal getLoyaltyDiscountAmount() {
return loyaltyDiscountAmount;
}
public void setLoyaltyDiscountAmount(BigDecimal loyaltyDiscountAmount) {
this.loyaltyDiscountAmount = loyaltyDiscountAmount;
}
public Integer getPointsEarned() { public Integer getPointsEarned() {
return pointsEarned; return pointsEarned;
} }
@@ -137,22 +145,6 @@ public class SaleResponse {
this.pointsEarned = pointsEarned; this.pointsEarned = pointsEarned;
} }
public Integer getPointsUsed() {
return pointsUsed;
}
public void setPointsUsed(Integer pointsUsed) {
this.pointsUsed = pointsUsed;
}
public BigDecimal getPointsDiscountAmount() {
return pointsDiscountAmount;
}
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
this.pointsDiscountAmount = pointsDiscountAmount;
}
public String getChannel() { public String getChannel() {
return channel; return channel;
} }

View File

@@ -1,11 +1,13 @@
package com.petshop.backend.entity; package com.petshop.backend.entity;
import jakarta.persistence.*; import jakarta.persistence.*;
import org.hibernate.annotations.Immutable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Objects; import java.util.Objects;
@Entity @Entity
@Immutable
@Table(name = "activityLog") @Table(name = "activityLog")
public class ActivityLog { public class ActivityLog {

View File

@@ -40,6 +40,24 @@ public class Cart {
@Column(nullable = false, precision = 10, scale = 2) @Column(nullable = false, precision = 10, scale = 2)
private BigDecimal totalAmount = BigDecimal.ZERO; private BigDecimal totalAmount = BigDecimal.ZERO;
@Column(nullable = false)
private Boolean pointsApplied = false;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal pointsDiscountAmount = BigDecimal.ZERO;
@Column(nullable = false)
private Boolean checkoutPending = false;
@Column(precision = 10, scale = 2)
private BigDecimal checkoutAmount;
@Column
private LocalDateTime checkoutStartedAt;
@Column(length = 255)
private String checkoutPaymentIntentId;
@CreationTimestamp @CreationTimestamp
@Column(name = "created_at", updatable = false) @Column(name = "created_at", updatable = false)
private LocalDateTime createdAt; private LocalDateTime createdAt;
@@ -115,6 +133,54 @@ public class Cart {
this.totalAmount = totalAmount; this.totalAmount = totalAmount;
} }
public Boolean getPointsApplied() {
return pointsApplied;
}
public void setPointsApplied(Boolean pointsApplied) {
this.pointsApplied = pointsApplied;
}
public BigDecimal getPointsDiscountAmount() {
return pointsDiscountAmount;
}
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
this.pointsDiscountAmount = pointsDiscountAmount;
}
public Boolean getCheckoutPending() {
return checkoutPending;
}
public void setCheckoutPending(Boolean checkoutPending) {
this.checkoutPending = checkoutPending;
}
public BigDecimal getCheckoutAmount() {
return checkoutAmount;
}
public void setCheckoutAmount(BigDecimal checkoutAmount) {
this.checkoutAmount = checkoutAmount;
}
public LocalDateTime getCheckoutStartedAt() {
return checkoutStartedAt;
}
public void setCheckoutStartedAt(LocalDateTime checkoutStartedAt) {
this.checkoutStartedAt = checkoutStartedAt;
}
public String getCheckoutPaymentIntentId() {
return checkoutPaymentIntentId;
}
public void setCheckoutPaymentIntentId(String checkoutPaymentIntentId) {
this.checkoutPaymentIntentId = checkoutPaymentIntentId;
}
public LocalDateTime getCreatedAt() { public LocalDateTime getCreatedAt() {
return createdAt; return createdAt;
} }

View File

@@ -0,0 +1,102 @@
package com.petshop.backend.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
import java.util.Objects;
@Entity
@Table(name = "passwordResetToken")
public class PasswordResetToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "userId", nullable = false)
private User user;
@Column(nullable = false, unique = true, length = 64)
private String tokenHash;
@Column(nullable = false)
private LocalDateTime expiresAt;
@Column
private LocalDateTime usedAt;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getTokenHash() {
return tokenHash;
}
public void setTokenHash(String tokenHash) {
this.tokenHash = tokenHash;
}
public LocalDateTime getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(LocalDateTime expiresAt) {
this.expiresAt = expiresAt;
}
public LocalDateTime getUsedAt() {
return usedAt;
}
public void setUsedAt(LocalDateTime usedAt) {
this.usedAt = usedAt;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PasswordResetToken that = (PasswordResetToken) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}

View File

@@ -66,14 +66,12 @@ public class Sale {
@Column(nullable = false, precision = 10, scale = 2) @Column(nullable = false, precision = 10, scale = 2)
private BigDecimal employeeDiscountAmount = BigDecimal.ZERO; private BigDecimal employeeDiscountAmount = BigDecimal.ZERO;
@Column(nullable = false)
private Integer pointsEarned = 0;
@Column(nullable = false)
private Integer pointsUsed = 0;
@Column(nullable = false, precision = 10, scale = 2) @Column(nullable = false, precision = 10, scale = 2)
private BigDecimal pointsDiscountAmount = BigDecimal.ZERO; private BigDecimal loyaltyDiscountAmount = BigDecimal.ZERO;
@Column(nullable = false)
private Integer pointsEarned = 0;
@OneToMany(mappedBy = "sale", cascade = CascadeType.ALL) @OneToMany(mappedBy = "sale", cascade = CascadeType.ALL)
private List<SaleItem> items = new ArrayList<>(); private List<SaleItem> items = new ArrayList<>();
@@ -209,6 +207,15 @@ public class Sale {
this.employeeDiscountAmount = employeeDiscountAmount; this.employeeDiscountAmount = employeeDiscountAmount;
} }
public BigDecimal getLoyaltyDiscountAmount() {
return loyaltyDiscountAmount;
}
public void setLoyaltyDiscountAmount(BigDecimal loyaltyDiscountAmount) {
this.loyaltyDiscountAmount = loyaltyDiscountAmount;
}
public Integer getPointsEarned() { public Integer getPointsEarned() {
return pointsEarned; return pointsEarned;
} }
@@ -217,22 +224,6 @@ public class Sale {
this.pointsEarned = pointsEarned; this.pointsEarned = pointsEarned;
} }
public Integer getPointsUsed() {
return pointsUsed;
}
public void setPointsUsed(Integer pointsUsed) {
this.pointsUsed = pointsUsed;
}
public BigDecimal getPointsDiscountAmount() {
return pointsDiscountAmount;
}
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) {
this.pointsDiscountAmount = pointsDiscountAmount;
}
public List<SaleItem> getItems() { public List<SaleItem> getItems() {
return items; return items;
} }

View File

@@ -0,0 +1,17 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.PasswordResetToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface PasswordResetTokenRepository extends JpaRepository<PasswordResetToken, Long> {
List<PasswordResetToken> findByUser_IdAndUsedAtIsNull(Long userId);
Optional<PasswordResetToken> findByTokenHashAndUsedAtIsNullAndExpiresAtAfter(String tokenHash, LocalDateTime now);
}

View File

@@ -9,6 +9,7 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List; import java.util.List;
import java.util.Optional;
@Repository @Repository
public interface SaleRepository extends JpaRepository<Sale, Long> { public interface SaleRepository extends JpaRepository<Sale, Long> {
@@ -25,4 +26,6 @@ public interface SaleRepository extends JpaRepository<Sale, Long> {
Page<Sale> searchSales(@Param("q") String query, @Param("paymentMethod") String paymentMethod, @Param("storeId") Long storeId, @Param("isRefund") Boolean isRefund, Pageable pageable); Page<Sale> searchSales(@Param("q") String query, @Param("paymentMethod") String paymentMethod, @Param("storeId") Long storeId, @Param("isRefund") Boolean isRefund, Pageable pageable);
List<Sale> findByOriginalSaleSaleId(Long originalSaleId); List<Sale> findByOriginalSaleSaleId(Long originalSaleId);
Optional<Sale> findByCartCartId(Long cartId);
} }

View File

@@ -50,7 +50,12 @@ public class SecurityConfig {
http.cors(cors -> cors.configurationSource(corsConfigurationSource())) http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/login", "/api/v1/auth/register").permitAll() .requestMatchers(
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/auth/forgot-password",
"/api/v1/auth/reset-password"
).permitAll()
.requestMatchers("/api/v1/health").permitAll() .requestMatchers("/api/v1/health").permitAll()
.requestMatchers("/ws/chat/**", "/ws/chat-sockjs/**").permitAll() .requestMatchers("/ws/chat/**", "/ws/chat-sockjs/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll()

View File

@@ -1,6 +1,7 @@
package com.petshop.backend.service; package com.petshop.backend.service;
import com.petshop.backend.entity.User; import com.petshop.backend.entity.User;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.PathResource; import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -8,6 +9,7 @@ import org.springframework.http.MediaTypeFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.PostConstruct;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -22,7 +24,15 @@ public class AvatarStorageService {
private static final String STORED_PREFIX = "/uploads/avatars/"; private static final String STORED_PREFIX = "/uploads/avatars/";
private static final String OWNER_ENDPOINT = "/api/v1/auth/me/avatar/file"; private static final String OWNER_ENDPOINT = "/api/v1/auth/me/avatar/file";
private final Path avatarDirectory = Paths.get("uploads", "avatars").toAbsolutePath().normalize(); @Value("${app.upload.base-dir:uploads}")
private String uploadBaseDir;
private Path avatarDirectory;
@PostConstruct
private void init() {
avatarDirectory = Paths.get(uploadBaseDir, "avatars").toAbsolutePath().normalize();
}
public String storeAvatar(MultipartFile file) throws IOException { public String storeAvatar(MultipartFile file) throws IOException {
Files.createDirectories(avatarDirectory); Files.createDirectories(avatarDirectory);
@@ -101,7 +111,7 @@ public class AvatarStorageService {
} }
String extension = originalFilename.substring(extensionIndex).toLowerCase(Locale.ROOT); String extension = originalFilename.substring(extensionIndex).toLowerCase(Locale.ROOT);
return switch (extension) { return switch (extension) {
case ".jpg", ".jpeg", ".png", ".gif" -> extension; case ".jpg", ".jpeg", ".png", ".gif", ".webp" -> extension;
default -> ".jpg"; default -> ".jpg";
}; };
} }

View File

@@ -1,6 +1,8 @@
package com.petshop.backend.service; package com.petshop.backend.service;
import com.petshop.backend.dto.cart.*; import com.petshop.backend.dto.cart.*;
import com.petshop.backend.dto.sale.SaleItemRequest;
import com.petshop.backend.dto.sale.SaleRequest;
import com.petshop.backend.entity.*; 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;
@@ -22,28 +24,36 @@ import java.util.List;
@Service @Service
public class CartService { public class CartService {
private static final int LOYALTY_POINTS_PER_DOLLAR = 20;
private final CartRepository cartRepository; private final CartRepository cartRepository;
private final CartItemRepository cartItemRepository; private final CartItemRepository cartItemRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final StoreRepository storeRepository; private final StoreRepository storeRepository;
private final ProductRepository productRepository; private final ProductRepository productRepository;
private final CouponRepository couponRepository; private final CouponRepository couponRepository;
private final SaleRepository saleRepository;
private final SaleService saleService;
@Value("${stripe.secret-key:}") @Value("${stripe.secret-key:}")
private String stripeSecretKey; private String stripeSecretKey;
public CartService(CartRepository cartRepository, public CartService(CartRepository cartRepository,
CartItemRepository cartItemRepository, CartItemRepository cartItemRepository,
UserRepository userRepository, UserRepository userRepository,
StoreRepository storeRepository, StoreRepository storeRepository,
ProductRepository productRepository, ProductRepository productRepository,
CouponRepository couponRepository) { CouponRepository couponRepository,
SaleRepository saleRepository,
SaleService saleService) {
this.cartRepository = cartRepository; this.cartRepository = cartRepository;
this.cartItemRepository = cartItemRepository; this.cartItemRepository = cartItemRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.storeRepository = storeRepository; this.storeRepository = storeRepository;
this.productRepository = productRepository; this.productRepository = productRepository;
this.couponRepository = couponRepository; this.couponRepository = couponRepository;
this.saleRepository = saleRepository;
this.saleService = saleService;
} }
@PostConstruct @PostConstruct
@@ -77,10 +87,14 @@ public class CartService {
newCart.setUser(user); newCart.setUser(user);
newCart.setStore(store); newCart.setStore(store);
newCart.setCartStatus("ACTIVE"); newCart.setCartStatus("ACTIVE");
newCart.setPointsApplied(false);
newCart.setPointsDiscountAmount(BigDecimal.ZERO);
return cartRepository.save(newCart); return cartRepository.save(newCart);
}); });
requireNotCheckoutPending(cart);
cartItemRepository.findByCartCartIdAndProductProdId(cart.getCartId(), product.getProdId()) cartItemRepository.findByCartCartIdAndProductProdId(cart.getCartId(), product.getProdId())
.ifPresentOrElse( .ifPresentOrElse(
existing -> existing.setQuantity(existing.getQuantity() + request.getQuantity()), existing -> existing.setQuantity(existing.getQuantity() + request.getQuantity()),
@@ -112,6 +126,8 @@ public class CartService {
throw new BusinessException("Cart is not active"); throw new BusinessException("Cart is not active");
} }
requireNotCheckoutPending(item.getCart());
if (request.getQuantity() < 1) { if (request.getQuantity() < 1) {
throw new BusinessException("Quantity must be at least 1"); throw new BusinessException("Quantity must be at least 1");
} }
@@ -136,6 +152,8 @@ public class CartService {
throw new BusinessException("Cart is not active"); throw new BusinessException("Cart is not active");
} }
requireNotCheckoutPending(item.getCart());
Cart cart = item.getCart(); Cart cart = item.getCart();
cartItemRepository.delete(item); cartItemRepository.delete(item);
recalculate(cart); recalculate(cart);
@@ -147,11 +165,14 @@ public class CartService {
public void clearCart(Long userId, Long storeId) { public void clearCart(Long userId, Long storeId) {
cartRepository.findActiveCartByUserAndStore(userId, storeId, "ACTIVE") cartRepository.findActiveCartByUserAndStore(userId, storeId, "ACTIVE")
.ifPresent(cart -> { .ifPresent(cart -> {
requireNotCheckoutPending(cart);
cartItemRepository.deleteByCartCartId(cart.getCartId()); cartItemRepository.deleteByCartCartId(cart.getCartId());
cart.setSubtotalAmount(BigDecimal.ZERO); cart.setSubtotalAmount(BigDecimal.ZERO);
cart.setDiscountAmount(BigDecimal.ZERO); cart.setDiscountAmount(BigDecimal.ZERO);
cart.setPointsDiscountAmount(BigDecimal.ZERO);
cart.setTotalAmount(BigDecimal.ZERO); cart.setTotalAmount(BigDecimal.ZERO);
cart.setCoupon(null); cart.setCoupon(null);
cart.setPointsApplied(false);
cartRepository.save(cart); cartRepository.save(cart);
}); });
} }
@@ -162,6 +183,8 @@ public class CartService {
.findActiveCartByUserAndStore(userId, storeId, "ACTIVE") .findActiveCartByUserAndStore(userId, storeId, "ACTIVE")
.orElseThrow(() -> new BusinessException("No active cart found")); .orElseThrow(() -> new BusinessException("No active cart found"));
requireNotCheckoutPending(cart);
Coupon coupon = couponRepository.findByCouponCodeIgnoreCase(couponCode) Coupon coupon = couponRepository.findByCouponCodeIgnoreCase(couponCode)
.orElseThrow(() -> new BusinessException("Invalid coupon code")); .orElseThrow(() -> new BusinessException("Invalid coupon code"));
@@ -189,12 +212,28 @@ public class CartService {
return toResponse(cart); return toResponse(cart);
} }
@Transactional
public CartResponse applyPoints(Long userId, Long storeId, Boolean useLoyaltyPoints) {
Cart cart = cartRepository
.findActiveCartByUserAndStore(userId, storeId, "ACTIVE")
.orElseThrow(() -> new BusinessException("No active cart found"));
requireNotCheckoutPending(cart);
cart.setPointsApplied(Boolean.TRUE.equals(useLoyaltyPoints));
recalculate(cart);
return toResponse(cart);
}
@Transactional @Transactional
public CheckoutResponse checkout(Long userId, Long storeId) { public CheckoutResponse checkout(Long userId, Long storeId) {
Cart cart = cartRepository Cart cart = cartRepository
.findActiveCartByUserAndStore(userId, storeId, "ACTIVE") .findActiveCartByUserAndStore(userId, storeId, "ACTIVE")
.orElseThrow(() -> new BusinessException("No active cart found")); .orElseThrow(() -> new BusinessException("No active cart found"));
if (Boolean.TRUE.equals(cart.getCheckoutPending())) {
throw new BusinessException("Checkout already in progress for this cart");
}
List<CartItem> items = cartItemRepository.findByCartCartId(cart.getCartId()); List<CartItem> items = cartItemRepository.findByCartCartId(cart.getCartId());
if (items.isEmpty()) { if (items.isEmpty()) {
throw new BusinessException("Cart is empty"); throw new BusinessException("Cart is empty");
@@ -219,6 +258,12 @@ public class CartService {
PaymentIntent intent = PaymentIntent.create(params); PaymentIntent intent = PaymentIntent.create(params);
cart.setCheckoutPending(true);
cart.setCheckoutAmount(cart.getTotalAmount());
cart.setCheckoutStartedAt(LocalDateTime.now());
cart.setCheckoutPaymentIntentId(intent.getId());
cartRepository.save(cart);
return new CheckoutResponse( return new CheckoutResponse(
cart.getCartId(), cart.getCartId(),
intent.getClientSecret(), intent.getClientSecret(),
@@ -242,7 +287,29 @@ public class CartService {
throw new BusinessException("Payment has not been completed"); throw new BusinessException("Payment has not been completed");
} }
Long cartId = Long.parseLong(intent.getMetadata().get("cartId")); String cartIdMetadata = intent.getMetadata() != null ? intent.getMetadata().get("cartId") : null;
String userIdMetadata = intent.getMetadata() != null ? intent.getMetadata().get("userId") : null;
if (cartIdMetadata == null || cartIdMetadata.isBlank()) {
throw new BusinessException("Payment metadata is missing cart information");
}
if (userIdMetadata == null || userIdMetadata.isBlank()) {
throw new BusinessException("Payment metadata is missing user information");
}
Long cartId;
Long metadataUserId;
try {
cartId = Long.parseLong(cartIdMetadata);
metadataUserId = Long.parseLong(userIdMetadata);
} catch (NumberFormatException ex) {
throw new BusinessException("Payment metadata is invalid");
}
if (!metadataUserId.equals(userId)) {
throw new BusinessException("Unauthorized");
}
Cart cart = cartRepository.findById(cartId) Cart cart = cartRepository.findById(cartId)
.orElseThrow(() -> new BusinessException("Cart not found")); .orElseThrow(() -> new BusinessException("Cart not found"));
@@ -250,7 +317,74 @@ public class CartService {
throw new BusinessException("Unauthorized"); throw new BusinessException("Unauthorized");
} }
if (!Boolean.TRUE.equals(cart.getCheckoutPending())) {
throw new BusinessException("Cart checkout was not initiated");
}
if (!paymentIntentId.equals(cart.getCheckoutPaymentIntentId())) {
throw new BusinessException("Payment intent mismatch");
}
if (cart.getCheckoutAmount() == null) {
throw new BusinessException("Checkout amount snapshot is missing");
}
if (!cart.getCartStatus().equals("ACTIVE")) {
throw new BusinessException("Cart is not in active state");
}
List<CartItem> items = cartItemRepository.findByCartCartId(cart.getCartId());
if (items.isEmpty()) {
throw new BusinessException("Cart items were removed during checkout");
}
BigDecimal recalculatedTotal = recalculateTotalAmount(cart);
if (recalculatedTotal.compareTo(cart.getCheckoutAmount()) != 0) {
throw new BusinessException("Cart total changed during checkout");
}
long storedAmountInCents = cart.getCheckoutAmount()
.multiply(BigDecimal.valueOf(100))
.setScale(0, RoundingMode.HALF_UP)
.longValue();
if (intent.getAmount() != storedAmountInCents) {
throw new BusinessException("Stripe charged amount does not match expected amount");
}
if (saleRepository.findByCartCartId(cart.getCartId()).isPresent()) {
cart.setCartStatus("CHECKED_OUT");
cart.setCheckoutPending(false);
cart.setCheckoutAmount(null);
cart.setCheckoutStartedAt(null);
cart.setCheckoutPaymentIntentId(null);
cartRepository.save(cart);
return;
}
SaleRequest saleRequest = new SaleRequest();
saleRequest.setStoreId(cart.getStore().getStoreId());
saleRequest.setCustomerId(cart.getUser().getId());
saleRequest.setCartId(cart.getCartId());
saleRequest.setCouponId(cart.getCoupon() != null ? cart.getCoupon().getCouponId() : null);
saleRequest.setPaymentMethod("Card");
saleRequest.setChannel("WEBSITE");
saleRequest.setItems(cartItemRepository.findByCartCartId(cart.getCartId()).stream()
.map(item -> {
SaleItemRequest saleItemRequest = new SaleItemRequest();
saleItemRequest.setProdId(item.getProduct().getProdId());
saleItemRequest.setQuantity(item.getQuantity());
return saleItemRequest;
})
.toList());
saleService.createSale(saleRequest);
cart.setCartStatus("CHECKED_OUT"); cart.setCartStatus("CHECKED_OUT");
cart.setCheckoutPending(false);
cart.setCheckoutAmount(null);
cart.setCheckoutStartedAt(null);
cart.setCheckoutPaymentIntentId(null);
cartRepository.save(cart); cartRepository.save(cart);
} catch (StripeException e) { } catch (StripeException e) {
@@ -258,6 +392,12 @@ public class CartService {
} }
} }
private void requireNotCheckoutPending(Cart cart) {
if (Boolean.TRUE.equals(cart.getCheckoutPending())) {
throw new BusinessException("Cannot modify cart while checkout is in progress");
}
}
private void recalculate(Cart cart) { private void recalculate(Cart cart) {
List<CartItem> items = cartItemRepository.findByCartCartId(cart.getCartId()); List<CartItem> items = cartItemRepository.findByCartCartId(cart.getCartId());
@@ -274,8 +414,8 @@ public class CartService {
if ("PERCENTAGE".equalsIgnoreCase(coupon.getDiscountType())) { if ("PERCENTAGE".equalsIgnoreCase(coupon.getDiscountType())) {
discount = subtotal.multiply(coupon.getDiscountValue()) discount = subtotal.multiply(coupon.getDiscountValue())
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
} }
else if ("FIXED".equalsIgnoreCase(coupon.getDiscountType())) { else if ("FIXED".equalsIgnoreCase(coupon.getDiscountType())) {
discount = coupon.getDiscountValue().min(subtotal); discount = coupon.getDiscountValue().min(subtotal);
} }
@@ -283,10 +423,61 @@ public class CartService {
discount = discount.max(BigDecimal.ZERO).min(subtotal); discount = discount.max(BigDecimal.ZERO).min(subtotal);
cart.setDiscountAmount(discount); cart.setDiscountAmount(discount);
cart.setTotalAmount(subtotal.subtract(discount).max(BigDecimal.ZERO));
BigDecimal remainingAfterCoupon = subtotal.subtract(discount).max(BigDecimal.ZERO);
BigDecimal pointsDiscount = calculatePointsDiscount(cart.getUser(), remainingAfterCoupon, Boolean.TRUE.equals(cart.getPointsApplied()));
cart.setPointsDiscountAmount(pointsDiscount);
cart.setTotalAmount(remainingAfterCoupon.subtract(pointsDiscount).max(BigDecimal.ZERO));
cartRepository.save(cart); cartRepository.save(cart);
} }
private BigDecimal recalculateTotalAmount(Cart cart) {
List<CartItem> items = cartItemRepository.findByCartCartId(cart.getCartId());
BigDecimal subtotal = items.stream()
.map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(i.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal discount = BigDecimal.ZERO;
Coupon coupon = cart.getCoupon();
if (coupon != null) {
if ("PERCENTAGE".equalsIgnoreCase(coupon.getDiscountType())) {
discount = subtotal.multiply(coupon.getDiscountValue())
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
}
else if ("FIXED".equalsIgnoreCase(coupon.getDiscountType())) {
discount = coupon.getDiscountValue().min(subtotal);
}
}
discount = discount.max(BigDecimal.ZERO).min(subtotal);
BigDecimal remainingAfterCoupon = subtotal.subtract(discount).max(BigDecimal.ZERO);
BigDecimal pointsDiscount = calculatePointsDiscount(cart.getUser(), remainingAfterCoupon, Boolean.TRUE.equals(cart.getPointsApplied()));
return remainingAfterCoupon.subtract(pointsDiscount).max(BigDecimal.ZERO);
}
private BigDecimal calculatePointsDiscount(User user, BigDecimal remainingAmount, boolean pointsApplied) {
if (!pointsApplied || user == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
int availablePoints = user.getLoyaltyPoints() != null ? user.getLoyaltyPoints() : 0;
int wholeDollars = availablePoints / LOYALTY_POINTS_PER_DOLLAR;
if (wholeDollars <= 0) {
return BigDecimal.ZERO;
}
BigDecimal maxRedeemable = remainingAmount.setScale(0, RoundingMode.DOWN);
return BigDecimal.valueOf(wholeDollars)
.min(maxRedeemable)
.setScale(2, RoundingMode.HALF_UP);
}
private CartResponse toResponse(Cart cart) { private CartResponse toResponse(Cart cart) {
List<CartItemResponse> itemResponses = cartItemRepository List<CartItemResponse> itemResponses = cartItemRepository
.findByCartCartId(cart.getCartId()) .findByCartCartId(cart.getCartId())
@@ -309,8 +500,11 @@ public class CartService {
response.setItems(itemResponses); response.setItems(itemResponses);
response.setSubtotalAmount(cart.getSubtotalAmount()); response.setSubtotalAmount(cart.getSubtotalAmount());
response.setDiscountAmount(cart.getDiscountAmount()); response.setDiscountAmount(cart.getDiscountAmount());
response.setPointsDiscountAmount(cart.getPointsDiscountAmount());
response.setTotalAmount(cart.getTotalAmount()); response.setTotalAmount(cart.getTotalAmount());
response.setCouponCode(cart.getCoupon() != null ? cart.getCoupon().getCouponCode() : null); response.setCouponCode(cart.getCoupon() != null ? cart.getCoupon().getCouponCode() : null);
response.setPointsApplied(cart.getPointsApplied());
response.setAvailableLoyaltyPoints(cart.getUser() != null ? cart.getUser().getLoyaltyPoints() : null);
return response; return response;
} }

View File

@@ -1,5 +1,6 @@
package com.petshop.backend.service; package com.petshop.backend.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.PathResource; import org.springframework.core.io.PathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -21,20 +22,31 @@ public class CatalogImageStorageService {
private static final String PET_PREFIX = "/uploads/pets/"; private static final String PET_PREFIX = "/uploads/pets/";
private static final String PRODUCT_PREFIX = "/uploads/products/"; private static final String PRODUCT_PREFIX = "/uploads/products/";
@Value("${app.upload.base-dir:uploads}")
private String uploadBaseDir;
public String storePetImage(MultipartFile file) throws IOException { public String storePetImage(MultipartFile file) throws IOException {
return storeImage(file, Paths.get("uploads", "pets").toAbsolutePath().normalize(), PET_PREFIX); return storeImage(file, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX);
} }
public String storeProductImage(MultipartFile file) throws IOException { public String storeProductImage(MultipartFile file) throws IOException {
return storeImage(file, Paths.get("uploads", "products").toAbsolutePath().normalize(), PRODUCT_PREFIX); return storeImage(file, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX);
} }
public Resource loadPetImage(String storedPath) { public Resource loadPetImage(String storedPath) {
return new PathResource(resolveStoredPath(storedPath, Paths.get("uploads", "pets").toAbsolutePath().normalize(), PET_PREFIX)); Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX));
if (!resource.exists()) {
throw new IllegalArgumentException("Image file was not found");
}
return resource;
} }
public Resource loadProductImage(String storedPath) { public Resource loadProductImage(String storedPath) {
return new PathResource(resolveStoredPath(storedPath, Paths.get("uploads", "products").toAbsolutePath().normalize(), PRODUCT_PREFIX)); Resource resource = new PathResource(resolveStoredPath(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX));
if (!resource.exists()) {
throw new IllegalArgumentException("Image file was not found");
}
return resource;
} }
public MediaType resolveMediaType(Resource resource) { public MediaType resolveMediaType(Resource resource) {
@@ -42,11 +54,11 @@ public class CatalogImageStorageService {
} }
public void deletePetImage(String storedPath) throws IOException { public void deletePetImage(String storedPath) throws IOException {
deleteImage(storedPath, Paths.get("uploads", "pets").toAbsolutePath().normalize(), PET_PREFIX); deleteImage(storedPath, Paths.get(uploadBaseDir, "pets").toAbsolutePath().normalize(), PET_PREFIX);
} }
public void deleteProductImage(String storedPath) throws IOException { public void deleteProductImage(String storedPath) throws IOException {
deleteImage(storedPath, Paths.get("uploads", "products").toAbsolutePath().normalize(), PRODUCT_PREFIX); deleteImage(storedPath, Paths.get(uploadBaseDir, "products").toAbsolutePath().normalize(), PRODUCT_PREFIX);
} }
private String storeImage(MultipartFile file, Path directory, String prefix) throws IOException { private String storeImage(MultipartFile file, Path directory, String prefix) throws IOException {
@@ -90,7 +102,7 @@ public class CatalogImageStorageService {
} }
String extension = originalFilename.substring(extensionIndex).toLowerCase(Locale.ROOT); String extension = originalFilename.substring(extensionIndex).toLowerCase(Locale.ROOT);
return switch (extension) { return switch (extension) {
case ".jpg", ".jpeg", ".png", ".gif" -> extension; case ".jpg", ".jpeg", ".png", ".gif", ".webp" -> extension;
default -> ".jpg"; default -> ".jpg";
}; };
} }

View File

@@ -0,0 +1,136 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.auth.ForgotPasswordResponse;
import com.petshop.backend.dto.auth.ResetPasswordResponse;
import com.petshop.backend.entity.PasswordResetToken;
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 org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.Optional;
@Service
public class PasswordResetService {
private static final int RESET_TOKEN_BYTES = 32;
private static final long RESET_TOKEN_MINUTES = 30;
private final PasswordResetTokenRepository passwordResetTokenRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final SecureRandom secureRandom = new SecureRandom();
public PasswordResetService(PasswordResetTokenRepository passwordResetTokenRepository,
UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.passwordResetTokenRepository = passwordResetTokenRepository;
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Transactional
public ForgotPasswordResponse createResetToken(String usernameOrEmail) {
String normalized = trimToNull(usernameOrEmail);
if (normalized == null) {
throw new BusinessException("Username or email is required");
}
Optional<User> user = userRepository.findByEmail(normalized)
.or(() -> userRepository.findByUsername(normalized));
if (user.isEmpty() || !Boolean.TRUE.equals(user.get().getActive())) {
return new ForgotPasswordResponse(
"If an account matches that username or email, a reset token has been generated.",
null
);
}
User managedUser = user.get();
LocalDateTime now = LocalDateTime.now();
invalidateOutstandingTokens(managedUser.getId(), now);
String rawToken = generateRawToken();
PasswordResetToken resetToken = new PasswordResetToken();
resetToken.setUser(managedUser);
resetToken.setTokenHash(hashToken(rawToken));
resetToken.setExpiresAt(now.plusMinutes(RESET_TOKEN_MINUTES));
passwordResetTokenRepository.save(resetToken);
return new ForgotPasswordResponse(
"If an account matches that username or email, a reset token has been generated.",
rawToken
);
}
@Transactional
public ResetPasswordResponse resetPassword(String rawToken, String newPassword) {
String normalizedToken = trimToNull(rawToken);
if (normalizedToken == null) {
throw new BusinessException("Reset token is required");
}
PasswordResetToken token = passwordResetTokenRepository
.findByTokenHashAndUsedAtIsNullAndExpiresAtAfter(hashToken(normalizedToken), LocalDateTime.now())
.orElseThrow(() -> new BusinessException("Reset token is invalid or has expired"));
User user = token.getUser();
if (!Boolean.TRUE.equals(user.getActive())) {
throw new BusinessException("User account is inactive");
}
LocalDateTime now = LocalDateTime.now();
user.setPassword(passwordEncoder.encode(newPassword));
user.setTokenVersion(user.getTokenVersion() + 1);
userRepository.save(user);
token.setUsedAt(now);
passwordResetTokenRepository.save(token);
invalidateOutstandingTokens(user.getId(), now);
return new ResetPasswordResponse("Password reset successfully");
}
private void invalidateOutstandingTokens(Long userId, LocalDateTime usedAt) {
for (PasswordResetToken token : passwordResetTokenRepository.findByUser_IdAndUsedAtIsNull(userId)) {
token.setUsedAt(usedAt);
}
}
private String generateRawToken() {
byte[] bytes = new byte[RESET_TOKEN_BYTES];
secureRandom.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
private String hashToken(String rawToken) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashed = digest.digest(rawToken.getBytes(StandardCharsets.UTF_8));
StringBuilder builder = new StringBuilder(hashed.length * 2);
for (byte value : hashed) {
builder.append(String.format("%02x", value));
}
return builder.toString();
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("SHA-256 is not available", ex);
}
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
}

View File

@@ -22,6 +22,7 @@ import java.util.List;
public class SaleService { public class SaleService {
private static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = new BigDecimal("0.10"); private static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = new BigDecimal("0.10");
private static final int LOYALTY_POINTS_PER_DOLLAR = 20;
private final SaleRepository saleRepository; private final SaleRepository saleRepository;
private final ProductRepository productRepository; private final ProductRepository productRepository;
@@ -56,7 +57,9 @@ public class SaleService {
@Transactional @Transactional
public SaleResponse createSale(SaleRequest request) { public SaleResponse createSale(SaleRequest request) {
User employee = AuthenticationHelper.getAuthenticatedUser(userRepository); User actor = AuthenticationHelper.getAuthenticatedUser(userRepository);
boolean websiteSale = request.getChannel() != null && request.getChannel().equalsIgnoreCase("WEBSITE");
User employee = websiteSale ? resolveWebsiteSaleEmployee(request.getStoreId()) : actor;
StoreLocation store = storeRepository.findById(request.getStoreId()) StoreLocation store = storeRepository.findById(request.getStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId())); .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
@@ -96,6 +99,11 @@ public class SaleService {
sale.setCustomer(customer); sale.setCustomer(customer);
} }
if (websiteSale && customer == null) {
customer = actor;
sale.setCustomer(customer);
}
if (sale.getIsRefund() && request.getOriginalSaleId() != null) { if (sale.getIsRefund() && request.getOriginalSaleId() != null) {
Sale originalSale = saleRepository.findById(request.getOriginalSaleId()) Sale originalSale = saleRepository.findById(request.getOriginalSaleId())
.orElseThrow(() -> new ResourceNotFoundException("Original sale not found with id: " + request.getOriginalSaleId())); .orElseThrow(() -> new ResourceNotFoundException("Original sale not found with id: " + request.getOriginalSaleId()));
@@ -152,34 +160,17 @@ public class SaleService {
saleItems.add(saleItem); saleItems.add(saleItem);
subtotalAmount = subtotalAmount.add(itemTotal); subtotalAmount = subtotalAmount.add(itemTotal);
} }
subtotalAmount = subtotalAmount.negate();
Sale originalSale = sale.getOriginalSale(); sale.setSubtotalAmount(subtotalAmount);
BigDecimal originalSubtotal = originalSale.getSubtotalAmount() != null sale.setTotalAmount(subtotalAmount);
? originalSale.getSubtotalAmount() sale.setCouponDiscountAmount(BigDecimal.ZERO);
: originalSale.getItems().stream() sale.setEmployeeDiscountAmount(BigDecimal.ZERO);
.map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(Math.abs(i.getQuantity())))) sale.setLoyaltyDiscountAmount(BigDecimal.ZERO);
.reduce(BigDecimal.ZERO, BigDecimal::add); sale.setPointsEarned(0);
BigDecimal refundRatio = originalSubtotal.compareTo(BigDecimal.ZERO) != 0
? subtotalAmount.divide(originalSubtotal, 10, RoundingMode.HALF_UP)
: BigDecimal.ONE;
BigDecimal refundTotal = originalSale.getTotalAmount().abs()
.multiply(refundRatio).setScale(2, RoundingMode.HALF_UP);
sale.setSubtotalAmount(subtotalAmount.negate());
sale.setTotalAmount(refundTotal.negate());
User refundCustomer = customer != null ? customer : originalSale.getCustomer();
if (refundCustomer != null) {
int pointsToRestore = BigDecimal.valueOf(originalSale.getPointsUsed())
.multiply(refundRatio).setScale(0, RoundingMode.FLOOR).intValue();
int pointsToDeduct = BigDecimal.valueOf(originalSale.getPointsEarned())
.multiply(refundRatio).setScale(0, RoundingMode.FLOOR).intValue();
refundCustomer.setLoyaltyPoints(refundCustomer.getLoyaltyPoints() + pointsToRestore - pointsToDeduct);
userRepository.save(refundCustomer);
}
} else { } else {
if (request.getItems() == null || request.getItems().isEmpty()) {
throw new BusinessException("At least one item is required");
}
for (var itemRequest : request.getItems()) { for (var itemRequest : request.getItems()) {
Product product = productRepository.findById(itemRequest.getProdId()) Product product = productRepository.findById(itemRequest.getProdId())
.orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId())); .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProdId()));
@@ -212,28 +203,22 @@ public class SaleService {
BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount); BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount);
sale.setCouponDiscountAmount(couponDiscount); sale.setCouponDiscountAmount(couponDiscount);
BigDecimal pointsDiscount = BigDecimal.ZERO; BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount));
int pointsUsed = 0;
if (customer != null && request.getPointsUsed() != null && request.getPointsUsed() > 0) {
if (customer.getLoyaltyPoints() < request.getPointsUsed()) {
throw new BusinessException("Customer does not have enough loyalty points");
}
pointsUsed = request.getPointsUsed();
pointsDiscount = calculatePointsDiscount(pointsUsed);
customer.setLoyaltyPoints(customer.getLoyaltyPoints() - pointsUsed);
}
sale.setPointsUsed(pointsUsed);
sale.setPointsDiscountAmount(pointsDiscount);
BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount).subtract(pointsDiscount));
sale.setEmployeeDiscountAmount(employeeDiscount); sale.setEmployeeDiscountAmount(employeeDiscount);
BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(pointsDiscount).subtract(employeeDiscount); boolean useLoyaltyPoints = sale.getCart() != null && Boolean.TRUE.equals(sale.getCart().getPointsApplied());
BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(customer, subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount), useLoyaltyPoints);
sale.setLoyaltyDiscountAmount(loyaltyDiscount);
BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount).subtract(loyaltyDiscount);
sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO)); sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO));
int pointsUsed = toPointsUsed(loyaltyDiscount);
sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue()); sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue());
if (customer != null) { if (customer != null) {
customer.setLoyaltyPoints(customer.getLoyaltyPoints() + sale.getPointsEarned()); int currentPoints = customer.getLoyaltyPoints() != null ? customer.getLoyaltyPoints() : 0;
int updatedPoints = currentPoints - pointsUsed + sale.getPointsEarned();
customer.setLoyaltyPoints(Math.max(updatedPoints, 0));
userRepository.save(customer); userRepository.save(customer);
} }
} }
@@ -277,10 +262,6 @@ public class SaleService {
return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP); return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP);
} }
private BigDecimal calculatePointsDiscount(int pointsUsed) {
return new BigDecimal(pointsUsed).divide(new BigDecimal("20"), 2, RoundingMode.HALF_UP);
}
private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) { private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) {
if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) { if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
@@ -293,6 +274,36 @@ public class SaleService {
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
private BigDecimal calculateLoyaltyDiscount(User customer, BigDecimal remainingAmount, boolean useLoyaltyPoints) {
if (!useLoyaltyPoints || customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
int availablePoints = customer.getLoyaltyPoints() != null ? customer.getLoyaltyPoints() : 0;
int wholeDollars = availablePoints / LOYALTY_POINTS_PER_DOLLAR;
if (wholeDollars <= 0) {
return BigDecimal.ZERO;
}
BigDecimal maxRedeemable = remainingAmount.setScale(0, RoundingMode.DOWN);
return BigDecimal.valueOf(wholeDollars)
.min(maxRedeemable)
.setScale(2, RoundingMode.HALF_UP);
}
private int toPointsUsed(BigDecimal loyaltyDiscount) {
if (loyaltyDiscount == null || loyaltyDiscount.compareTo(BigDecimal.ZERO) <= 0) {
return 0;
}
return loyaltyDiscount.setScale(0, RoundingMode.DOWN).intValue() * LOYALTY_POINTS_PER_DOLLAR;
}
private User resolveWebsiteSaleEmployee(Long storeId) {
return userRepository.findFirstByPrimaryStoreStoreIdAndRoleAndActiveTrueOrderByIdAsc(storeId, User.Role.STAFF)
.or(() -> userRepository.findFirstByRoleAndActiveTrueOrderByIdAsc(User.Role.ADMIN))
.orElseThrow(() -> new BusinessException("No active employee available for website sale"));
}
private SaleResponse mapToResponse(Sale sale) { private SaleResponse mapToResponse(Sale sale) {
SaleResponse response = new SaleResponse(); SaleResponse response = new SaleResponse();
response.setSaleId(sale.getSaleId()); response.setSaleId(sale.getSaleId());
@@ -314,9 +325,8 @@ public class SaleService {
response.setSubtotalAmount(sale.getSubtotalAmount()); response.setSubtotalAmount(sale.getSubtotalAmount());
response.setCouponDiscountAmount(sale.getCouponDiscountAmount()); response.setCouponDiscountAmount(sale.getCouponDiscountAmount());
response.setEmployeeDiscountAmount(sale.getEmployeeDiscountAmount()); response.setEmployeeDiscountAmount(sale.getEmployeeDiscountAmount());
response.setLoyaltyDiscountAmount(sale.getLoyaltyDiscountAmount());
response.setPointsEarned(sale.getPointsEarned()); response.setPointsEarned(sale.getPointsEarned());
response.setPointsUsed(sale.getPointsUsed());
response.setPointsDiscountAmount(sale.getPointsDiscountAmount());
response.setChannel(sale.getChannel()); response.setChannel(sale.getChannel());
if (sale.getCoupon() != null) { if (sale.getCoupon() != null) {
response.setCouponId(sale.getCoupon().getCouponId()); response.setCouponId(sale.getCoupon().getCouponId());

View File

@@ -47,6 +47,10 @@ springdoc:
swagger-ui: swagger-ui:
path: /swagger-ui path: /swagger-ui
app:
upload:
base-dir: ${UPLOAD_BASE_DIR:uploads}
jwt: jwt:
secret: ${JWT_SECRET} secret: ${JWT_SECRET}
expiration: ${JWT_EXPIRATION:86400000} expiration: ${JWT_EXPIRATION:86400000}

View File

@@ -1,4 +1,3 @@
CREATE TABLE IF NOT EXISTS storeLocation ( CREATE TABLE IF NOT EXISTS storeLocation (
storeId BIGINT AUTO_INCREMENT PRIMARY KEY, storeId BIGINT AUTO_INCREMENT PRIMARY KEY,
storeName VARCHAR(100) NOT NULL, storeName VARCHAR(100) NOT NULL,
@@ -192,6 +191,12 @@ CREATE TABLE IF NOT EXISTS cart (
subtotalAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, subtotalAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
discountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, discountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
totalAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, totalAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
pointsApplied BOOLEAN NOT NULL DEFAULT FALSE,
pointsDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
checkoutPending BOOLEAN NOT NULL DEFAULT FALSE,
checkoutAmount DECIMAL(10, 2),
checkoutStartedAt DATETIME,
checkoutPaymentIntentId VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_cart_user FOREIGN KEY (userId) REFERENCES users(id), CONSTRAINT fk_cart_user FOREIGN KEY (userId) REFERENCES users(id),
@@ -227,9 +232,13 @@ CREATE TABLE IF NOT EXISTS sale (
subtotalAmount DECIMAL(10, 2) NULL, subtotalAmount DECIMAL(10, 2) NULL,
couponDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, couponDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
employeeDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00, employeeDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
pointsUsed INT NOT NULL DEFAULT 0,
loyaltyDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
pointsEarned INT NOT NULL DEFAULT 0, pointsEarned INT NOT NULL DEFAULT 0,
pointsDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT uq_sale_cart_id UNIQUE (cartId),
CONSTRAINT fk_sale_employee FOREIGN KEY (employeeId) REFERENCES users(id), CONSTRAINT fk_sale_employee FOREIGN KEY (employeeId) REFERENCES users(id),
CONSTRAINT fk_sale_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), CONSTRAINT fk_sale_store FOREIGN KEY (storeId) REFERENCES storeLocation(storeId),
CONSTRAINT fk_sale_customer FOREIGN KEY (customerId) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT fk_sale_customer FOREIGN KEY (customerId) REFERENCES users(id) ON DELETE SET NULL,
@@ -275,6 +284,20 @@ CREATE TABLE IF NOT EXISTS refund_item (
CONSTRAINT fk_refund_item_product FOREIGN KEY (prod_id) REFERENCES product(prodId) CONSTRAINT fk_refund_item_product FOREIGN KEY (prod_id) REFERENCES product(prodId)
); );
CREATE TABLE IF NOT EXISTS passwordResetToken (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
userId BIGINT NOT NULL,
tokenHash VARCHAR(64) NOT NULL,
expiresAt DATETIME NOT NULL,
usedAt DATETIME NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_password_reset_token_hash UNIQUE (tokenHash),
CONSTRAINT fk_password_reset_token_user FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX idx_password_reset_token_user ON passwordResetToken(userId);
CREATE INDEX idx_password_reset_token_expires ON passwordResetToken(expiresAt);
CREATE TABLE IF NOT EXISTS conversation ( CREATE TABLE IF NOT EXISTS conversation (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY,
customerId BIGINT NOT NULL, customerId BIGINT NOT NULL,
@@ -307,6 +330,10 @@ CREATE TABLE IF NOT EXISTS activityLog (
logId BIGINT AUTO_INCREMENT PRIMARY KEY, logId BIGINT AUTO_INCREMENT PRIMARY KEY,
userId BIGINT NOT NULL, userId BIGINT NOT NULL,
storeId BIGINT NULL, storeId BIGINT NULL,
usernameSnapshot VARCHAR(50) NULL,
fullNameSnapshot VARCHAR(100) NULL,
roleSnapshot VARCHAR(20) NULL,
storeNameSnapshot VARCHAR(100) NULL,
activity TEXT NOT NULL, activity TEXT NOT NULL,
logTimestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, logTimestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_activity_log_user FOREIGN KEY (userId) REFERENCES users(id), CONSTRAINT fk_activity_log_user FOREIGN KEY (userId) REFERENCES users(id),
@@ -339,3 +366,4 @@ CREATE INDEX idx_cart_user ON cart(userId);
CREATE INDEX idx_conversation_customer ON conversation(customerId); CREATE INDEX idx_conversation_customer ON conversation(customerId);
CREATE INDEX idx_conversation_staff ON conversation(staffId); CREATE INDEX idx_conversation_staff ON conversation(staffId);
CREATE INDEX idx_activity_log_store ON activityLog(storeId); CREATE INDEX idx_activity_log_store ON activityLog(storeId);
CREATE INDEX idx_activity_log_timestamp_id ON activityLog(logTimestamp, logId);

View File

@@ -4,6 +4,7 @@ SET FOREIGN_KEY_CHECKS = 0;
DELETE FROM activityLog; DELETE FROM activityLog;
DELETE FROM message; DELETE FROM message;
DELETE FROM conversation; DELETE FROM conversation;
DELETE FROM passwordResetToken;
DELETE FROM refund_item; DELETE FROM refund_item;
DELETE FROM refund; DELETE FROM refund;
DELETE FROM saleItem; DELETE FROM saleItem;
@@ -45,6 +46,7 @@ ALTER TABLE refund AUTO_INCREMENT = 1;
ALTER TABLE refund_item AUTO_INCREMENT = 1; ALTER TABLE refund_item AUTO_INCREMENT = 1;
ALTER TABLE conversation AUTO_INCREMENT = 1; ALTER TABLE conversation AUTO_INCREMENT = 1;
ALTER TABLE message AUTO_INCREMENT = 1; ALTER TABLE message AUTO_INCREMENT = 1;
ALTER TABLE passwordResetToken AUTO_INCREMENT = 1;
ALTER TABLE activityLog AUTO_INCREMENT = 1; ALTER TABLE activityLog AUTO_INCREMENT = 1;
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;
@@ -1739,3 +1741,509 @@ INSERT INTO message (id, conversationId, senderId, content, attachmentUrl, attac
(118, 30, 8, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-03-02 09:05:00', 1), (118, 30, 8, 'Happy to help. Please share the order number or the pet name involved.', NULL, NULL, NULL, NULL, '2026-03-02 09:05:00', 1),
(119, 30, 45, 'Order #1030 is the one I meant, and the pet is Kiki.', NULL, NULL, NULL, NULL, '2026-03-02 09:10:00', 1), (119, 30, 45, 'Order #1030 is the one I meant, and the pet is Kiki.', NULL, NULL, NULL, NULL, '2026-03-02 09:10:00', 1),
(120, 30, 8, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-03-02 09:15:00', 0); (120, 30, 8, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-03-02 09:15:00', 0);
INSERT INTO users (username, password, email, firstName, lastName, fullName, phone, avatarUrl, role, staffRole, primaryStoreId, loyaltyPoints, active, tokenVersion)
SELECT 'ai.bot',
'$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq',
'bot@petshop.com',
'AI',
'Bot',
'AI Bot',
'000-000-0000',
'https://images.petshop.local/users/bot.webp',
'STAFF',
'CUSTOMER_SERVICE',
NULL,
0,
1,
0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1
FROM users
WHERE username = 'ai.bot'
);
UPDATE users
SET
username = CASE id
WHEN 15 THEN 'customer'
WHEN 16 THEN 'maya.brown'
WHEN 17 THEN 'noah.clark'
WHEN 18 THEN 'avery.wilson'
WHEN 19 THEN 'leah.martinez'
WHEN 20 THEN 'julian.anderson'
WHEN 21 THEN 'zoe.taylor'
WHEN 22 THEN 'ethan.parker'
WHEN 23 THEN 'ruby.evans'
WHEN 24 THEN 'caleb.scott'
WHEN 25 THEN 'ivy.adams'
WHEN 26 THEN 'isaac.baker'
WHEN 27 THEN 'hannah.hall'
WHEN 28 THEN 'mason.rivera'
WHEN 29 THEN 'aria.mitchell'
WHEN 30 THEN 'wyatt.collins'
WHEN 31 THEN 'elena.morris'
WHEN 32 THEN 'leo.cook'
WHEN 33 THEN 'grace.bell'
WHEN 34 THEN 'hudson.reed'
WHEN 35 THEN 'claire.murphy'
WHEN 36 THEN 'omar.bailey'
WHEN 37 THEN 'naomi.cooper'
WHEN 38 THEN 'jasper.richardson'
WHEN 39 THEN 'sofia.cox'
WHEN 40 THEN 'miles.howard'
WHEN 41 THEN 'audrey.ward'
WHEN 42 THEN 'nathan.torres'
WHEN 43 THEN 'jade.peterson'
WHEN 44 THEN 'rowan.gray'
WHEN 45 THEN 'lila.ramirez'
WHEN 46 THEN 'eli.james'
WHEN 47 THEN 'violet.watson'
WHEN 48 THEN 'gavin.brooks'
WHEN 49 THEN 'stella.kelly'
WHEN 50 THEN 'adrian.sanders'
WHEN 51 THEN 'hazel.price'
WHEN 52 THEN 'connor.bennett'
WHEN 53 THEN 'sadie.wood'
WHEN 54 THEN 'xavier.barnes'
WHEN 55 THEN 'alice.ross'
WHEN 56 THEN 'roman.henderson'
WHEN 57 THEN 'lucy.coleman'
WHEN 58 THEN 'evan.jenkins'
WHEN 59 THEN 'mila.perry'
WHEN 60 THEN 'cole.powell'
WHEN 61 THEN 'nora.long'
WHEN 62 THEN 'adam.patterson'
WHEN 63 THEN 'layla.hughes'
WHEN 64 THEN 'blake.flores'
WHEN 65 THEN 'ellie.washington'
WHEN 66 THEN 'ryan.butler'
WHEN 67 THEN 'cora.simmons'
WHEN 68 THEN 'simon.foster'
WHEN 69 THEN 'piper.gonzales'
WHEN 70 THEN 'joel.bryant'
WHEN 71 THEN 'eva.alexander'
WHEN 72 THEN 'felix.russell'
WHEN 73 THEN 'maeve.griffin'
WHEN 74 THEN 'tristan.diaz'
WHEN 75 THEN 'ariana.hayes'
WHEN 76 THEN 'declan.myers'
WHEN 77 THEN 'brooke.ford'
WHEN 78 THEN 'micah.hamilton'
WHEN 79 THEN 'bianca.graham'
WHEN 80 THEN 'jonah.sullivan'
WHEN 81 THEN 'tessa.wallace'
WHEN 82 THEN 'damian.woods'
WHEN 83 THEN 'riley.cole'
WHEN 84 THEN 'kieran.west'
WHEN 85 THEN 'sienna.jordan'
WHEN 86 THEN 'finley.owens'
WHEN 87 THEN 'maren.reynolds'
WHEN 88 THEN 'asher.fisher'
WHEN 89 THEN 'daphne.ellis'
WHEN 90 THEN 'bennett.harrison'
WHEN 91 THEN 'selena.gibson'
WHEN 92 THEN 'emmett.mcdonald'
WHEN 93 THEN 'phoebe.cruz'
WHEN 94 THEN 'sawyer.marshall'
WHEN 95 THEN 'keira.ortiz'
WHEN 96 THEN 'landon.gomez'
WHEN 97 THEN 'rosalie.murray'
WHEN 98 THEN 'malik.freeman'
WHEN 99 THEN 'esme.wells'
WHEN 100 THEN 'holden.webb'
ELSE username
END,
email = CASE id
WHEN 15 THEN 'customer@petshop.com'
WHEN 16 THEN 'maya.brown@gmail.com'
WHEN 17 THEN 'noah.clark@gmail.com'
WHEN 18 THEN 'avery.wilson@gmail.com'
WHEN 19 THEN 'leah.martinez@gmail.com'
WHEN 20 THEN 'julian.anderson@gmail.com'
WHEN 21 THEN 'zoe.taylor@gmail.com'
WHEN 22 THEN 'ethan.parker@gmail.com'
WHEN 23 THEN 'ruby.evans@gmail.com'
WHEN 24 THEN 'caleb.scott@gmail.com'
WHEN 25 THEN 'ivy.adams@gmail.com'
WHEN 26 THEN 'isaac.baker@gmail.com'
WHEN 27 THEN 'hannah.hall@gmail.com'
WHEN 28 THEN 'mason.rivera@gmail.com'
WHEN 29 THEN 'aria.mitchell@gmail.com'
WHEN 30 THEN 'wyatt.collins@gmail.com'
WHEN 31 THEN 'elena.morris@gmail.com'
WHEN 32 THEN 'leo.cook@gmail.com'
WHEN 33 THEN 'grace.bell@gmail.com'
WHEN 34 THEN 'hudson.reed@gmail.com'
WHEN 35 THEN 'claire.murphy@gmail.com'
WHEN 36 THEN 'omar.bailey@gmail.com'
WHEN 37 THEN 'naomi.cooper@gmail.com'
WHEN 38 THEN 'jasper.richardson@gmail.com'
WHEN 39 THEN 'sofia.cox@gmail.com'
WHEN 40 THEN 'miles.howard@gmail.com'
WHEN 41 THEN 'audrey.ward@gmail.com'
WHEN 42 THEN 'nathan.torres@gmail.com'
WHEN 43 THEN 'jade.peterson@gmail.com'
WHEN 44 THEN 'rowan.gray@gmail.com'
WHEN 45 THEN 'lila.ramirez@gmail.com'
WHEN 46 THEN 'eli.james@gmail.com'
WHEN 47 THEN 'violet.watson@gmail.com'
WHEN 48 THEN 'gavin.brooks@gmail.com'
WHEN 49 THEN 'stella.kelly@gmail.com'
WHEN 50 THEN 'adrian.sanders@gmail.com'
WHEN 51 THEN 'hazel.price@gmail.com'
WHEN 52 THEN 'connor.bennett@gmail.com'
WHEN 53 THEN 'sadie.wood@gmail.com'
WHEN 54 THEN 'xavier.barnes@gmail.com'
WHEN 55 THEN 'alice.ross@gmail.com'
WHEN 56 THEN 'roman.henderson@gmail.com'
WHEN 57 THEN 'lucy.coleman@gmail.com'
WHEN 58 THEN 'evan.jenkins@gmail.com'
WHEN 59 THEN 'mila.perry@gmail.com'
WHEN 60 THEN 'cole.powell@gmail.com'
WHEN 61 THEN 'nora.long@gmail.com'
WHEN 62 THEN 'adam.patterson@gmail.com'
WHEN 63 THEN 'layla.hughes@gmail.com'
WHEN 64 THEN 'blake.flores@gmail.com'
WHEN 65 THEN 'ellie.washington@gmail.com'
WHEN 66 THEN 'ryan.butler@gmail.com'
WHEN 67 THEN 'cora.simmons@gmail.com'
WHEN 68 THEN 'simon.foster@gmail.com'
WHEN 69 THEN 'piper.gonzales@gmail.com'
WHEN 70 THEN 'joel.bryant@gmail.com'
WHEN 71 THEN 'eva.alexander@gmail.com'
WHEN 72 THEN 'felix.russell@gmail.com'
WHEN 73 THEN 'maeve.griffin@gmail.com'
WHEN 74 THEN 'tristan.diaz@gmail.com'
WHEN 75 THEN 'ariana.hayes@gmail.com'
WHEN 76 THEN 'declan.myers@gmail.com'
WHEN 77 THEN 'brooke.ford@gmail.com'
WHEN 78 THEN 'micah.hamilton@gmail.com'
WHEN 79 THEN 'bianca.graham@gmail.com'
WHEN 80 THEN 'jonah.sullivan@gmail.com'
WHEN 81 THEN 'tessa.wallace@gmail.com'
WHEN 82 THEN 'damian.woods@gmail.com'
WHEN 83 THEN 'riley.cole@gmail.com'
WHEN 84 THEN 'kieran.west@gmail.com'
WHEN 85 THEN 'sienna.jordan@gmail.com'
WHEN 86 THEN 'finley.owens@gmail.com'
WHEN 87 THEN 'maren.reynolds@gmail.com'
WHEN 88 THEN 'asher.fisher@gmail.com'
WHEN 89 THEN 'daphne.ellis@gmail.com'
WHEN 90 THEN 'bennett.harrison@gmail.com'
WHEN 91 THEN 'selena.gibson@gmail.com'
WHEN 92 THEN 'emmett.mcdonald@gmail.com'
WHEN 93 THEN 'phoebe.cruz@gmail.com'
WHEN 94 THEN 'sawyer.marshall@gmail.com'
WHEN 95 THEN 'keira.ortiz@gmail.com'
WHEN 96 THEN 'landon.gomez@gmail.com'
WHEN 97 THEN 'rosalie.murray@gmail.com'
WHEN 98 THEN 'malik.freeman@gmail.com'
WHEN 99 THEN 'esme.wells@gmail.com'
WHEN 100 THEN 'holden.webb@gmail.com'
ELSE email
END,
firstName = CASE id
WHEN 15 THEN 'Test'
WHEN 16 THEN 'Maya'
WHEN 17 THEN 'Noah'
WHEN 18 THEN 'Avery'
WHEN 19 THEN 'Leah'
WHEN 20 THEN 'Julian'
WHEN 21 THEN 'Zoe'
WHEN 22 THEN 'Ethan'
WHEN 23 THEN 'Ruby'
WHEN 24 THEN 'Caleb'
WHEN 25 THEN 'Ivy'
WHEN 26 THEN 'Isaac'
WHEN 27 THEN 'Hannah'
WHEN 28 THEN 'Mason'
WHEN 29 THEN 'Aria'
WHEN 30 THEN 'Wyatt'
WHEN 31 THEN 'Elena'
WHEN 32 THEN 'Leo'
WHEN 33 THEN 'Grace'
WHEN 34 THEN 'Hudson'
WHEN 35 THEN 'Claire'
WHEN 36 THEN 'Omar'
WHEN 37 THEN 'Naomi'
WHEN 38 THEN 'Jasper'
WHEN 39 THEN 'Sofia'
WHEN 40 THEN 'Miles'
WHEN 41 THEN 'Audrey'
WHEN 42 THEN 'Nathan'
WHEN 43 THEN 'Jade'
WHEN 44 THEN 'Rowan'
WHEN 45 THEN 'Lila'
WHEN 46 THEN 'Eli'
WHEN 47 THEN 'Violet'
WHEN 48 THEN 'Gavin'
WHEN 49 THEN 'Stella'
WHEN 50 THEN 'Adrian'
WHEN 51 THEN 'Hazel'
WHEN 52 THEN 'Connor'
WHEN 53 THEN 'Sadie'
WHEN 54 THEN 'Xavier'
WHEN 55 THEN 'Alice'
WHEN 56 THEN 'Roman'
WHEN 57 THEN 'Lucy'
WHEN 58 THEN 'Evan'
WHEN 59 THEN 'Mila'
WHEN 60 THEN 'Cole'
WHEN 61 THEN 'Nora'
WHEN 62 THEN 'Adam'
WHEN 63 THEN 'Layla'
WHEN 64 THEN 'Blake'
WHEN 65 THEN 'Ellie'
WHEN 66 THEN 'Ryan'
WHEN 67 THEN 'Cora'
WHEN 68 THEN 'Simon'
WHEN 69 THEN 'Piper'
WHEN 70 THEN 'Joel'
WHEN 71 THEN 'Eva'
WHEN 72 THEN 'Felix'
WHEN 73 THEN 'Maeve'
WHEN 74 THEN 'Tristan'
WHEN 75 THEN 'Ariana'
WHEN 76 THEN 'Declan'
WHEN 77 THEN 'Brooke'
WHEN 78 THEN 'Micah'
WHEN 79 THEN 'Bianca'
WHEN 80 THEN 'Jonah'
WHEN 81 THEN 'Tessa'
WHEN 82 THEN 'Damian'
WHEN 83 THEN 'Riley'
WHEN 84 THEN 'Kieran'
WHEN 85 THEN 'Sienna'
WHEN 86 THEN 'Finley'
WHEN 87 THEN 'Maren'
WHEN 88 THEN 'Asher'
WHEN 89 THEN 'Daphne'
WHEN 90 THEN 'Bennett'
WHEN 91 THEN 'Selena'
WHEN 92 THEN 'Emmett'
WHEN 93 THEN 'Phoebe'
WHEN 94 THEN 'Sawyer'
WHEN 95 THEN 'Keira'
WHEN 96 THEN 'Landon'
WHEN 97 THEN 'Rosalie'
WHEN 98 THEN 'Malik'
WHEN 99 THEN 'Esme'
WHEN 100 THEN 'Holden'
ELSE firstName
END,
fullName = CASE id
WHEN 15 THEN 'Test Customer'
WHEN 16 THEN 'Maya Brown'
WHEN 17 THEN 'Noah Clark'
WHEN 18 THEN 'Avery Wilson'
WHEN 19 THEN 'Leah Martinez'
WHEN 20 THEN 'Julian Anderson'
WHEN 21 THEN 'Zoe Taylor'
WHEN 22 THEN 'Ethan Parker'
WHEN 23 THEN 'Ruby Evans'
WHEN 24 THEN 'Caleb Scott'
WHEN 25 THEN 'Ivy Adams'
WHEN 26 THEN 'Isaac Baker'
WHEN 27 THEN 'Hannah Hall'
WHEN 28 THEN 'Mason Rivera'
WHEN 29 THEN 'Aria Mitchell'
WHEN 30 THEN 'Wyatt Collins'
WHEN 31 THEN 'Elena Morris'
WHEN 32 THEN 'Leo Cook'
WHEN 33 THEN 'Grace Bell'
WHEN 34 THEN 'Hudson Reed'
WHEN 35 THEN 'Claire Murphy'
WHEN 36 THEN 'Omar Bailey'
WHEN 37 THEN 'Naomi Cooper'
WHEN 38 THEN 'Jasper Richardson'
WHEN 39 THEN 'Sofia Cox'
WHEN 40 THEN 'Miles Howard'
WHEN 41 THEN 'Audrey Ward'
WHEN 42 THEN 'Nathan Torres'
WHEN 43 THEN 'Jade Peterson'
WHEN 44 THEN 'Rowan Gray'
WHEN 45 THEN 'Lila Ramirez'
WHEN 46 THEN 'Eli James'
WHEN 47 THEN 'Violet Watson'
WHEN 48 THEN 'Gavin Brooks'
WHEN 49 THEN 'Stella Kelly'
WHEN 50 THEN 'Adrian Sanders'
WHEN 51 THEN 'Hazel Price'
WHEN 52 THEN 'Connor Bennett'
WHEN 53 THEN 'Sadie Wood'
WHEN 54 THEN 'Xavier Barnes'
WHEN 55 THEN 'Alice Ross'
WHEN 56 THEN 'Roman Henderson'
WHEN 57 THEN 'Lucy Coleman'
WHEN 58 THEN 'Evan Jenkins'
WHEN 59 THEN 'Mila Perry'
WHEN 60 THEN 'Cole Powell'
WHEN 61 THEN 'Nora Long'
WHEN 62 THEN 'Adam Patterson'
WHEN 63 THEN 'Layla Hughes'
WHEN 64 THEN 'Blake Flores'
WHEN 65 THEN 'Ellie Washington'
WHEN 66 THEN 'Ryan Butler'
WHEN 67 THEN 'Cora Simmons'
WHEN 68 THEN 'Simon Foster'
WHEN 69 THEN 'Piper Gonzales'
WHEN 70 THEN 'Joel Bryant'
WHEN 71 THEN 'Eva Alexander'
WHEN 72 THEN 'Felix Russell'
WHEN 73 THEN 'Maeve Griffin'
WHEN 74 THEN 'Tristan Diaz'
WHEN 75 THEN 'Ariana Hayes'
WHEN 76 THEN 'Declan Myers'
WHEN 77 THEN 'Brooke Ford'
WHEN 78 THEN 'Micah Hamilton'
WHEN 79 THEN 'Bianca Graham'
WHEN 80 THEN 'Jonah Sullivan'
WHEN 81 THEN 'Tessa Wallace'
WHEN 82 THEN 'Damian Woods'
WHEN 83 THEN 'Riley Cole'
WHEN 84 THEN 'Kieran West'
WHEN 85 THEN 'Sienna Jordan'
WHEN 86 THEN 'Finley Owens'
WHEN 87 THEN 'Maren Reynolds'
WHEN 88 THEN 'Asher Fisher'
WHEN 89 THEN 'Daphne Ellis'
WHEN 90 THEN 'Bennett Harrison'
WHEN 91 THEN 'Selena Gibson'
WHEN 92 THEN 'Emmett Mcdonald'
WHEN 93 THEN 'Phoebe Cruz'
WHEN 94 THEN 'Sawyer Marshall'
WHEN 95 THEN 'Keira Ortiz'
WHEN 96 THEN 'Landon Gomez'
WHEN 97 THEN 'Rosalie Murray'
WHEN 98 THEN 'Malik Freeman'
WHEN 99 THEN 'Esme Wells'
WHEN 100 THEN 'Holden Webb'
ELSE fullName
END,
primaryStoreId = CASE id
WHEN 15 THEN 1
WHEN 16 THEN 2
WHEN 17 THEN 3
WHEN 18 THEN 1
WHEN 19 THEN 2
WHEN 20 THEN 3
WHEN 21 THEN 1
WHEN 22 THEN 2
WHEN 23 THEN 3
WHEN 24 THEN 1
WHEN 25 THEN 2
WHEN 26 THEN 3
WHEN 27 THEN 1
WHEN 28 THEN 2
WHEN 29 THEN 3
WHEN 30 THEN 1
WHEN 31 THEN 2
WHEN 32 THEN 3
WHEN 33 THEN 1
WHEN 34 THEN 2
WHEN 35 THEN 3
WHEN 36 THEN 1
WHEN 37 THEN 2
WHEN 38 THEN 3
WHEN 39 THEN 1
WHEN 40 THEN 2
WHEN 41 THEN 3
WHEN 42 THEN 1
WHEN 43 THEN 2
WHEN 44 THEN 3
WHEN 45 THEN 1
WHEN 46 THEN 2
WHEN 47 THEN 3
WHEN 48 THEN 1
WHEN 49 THEN 2
WHEN 50 THEN 3
WHEN 51 THEN 1
WHEN 52 THEN 2
WHEN 53 THEN 3
WHEN 54 THEN 1
WHEN 55 THEN 2
WHEN 56 THEN 3
WHEN 57 THEN 1
WHEN 58 THEN 2
WHEN 59 THEN 3
WHEN 60 THEN 1
WHEN 61 THEN 2
WHEN 62 THEN 3
WHEN 63 THEN 1
WHEN 64 THEN 2
WHEN 65 THEN 3
WHEN 66 THEN 1
WHEN 67 THEN 2
WHEN 68 THEN 3
WHEN 69 THEN 1
WHEN 70 THEN 2
WHEN 71 THEN 3
WHEN 72 THEN 1
WHEN 73 THEN 2
WHEN 74 THEN 3
WHEN 75 THEN 1
WHEN 76 THEN 2
WHEN 77 THEN 3
WHEN 78 THEN 1
WHEN 79 THEN 2
WHEN 80 THEN 3
WHEN 81 THEN 1
WHEN 82 THEN 2
WHEN 83 THEN 3
WHEN 84 THEN 1
WHEN 85 THEN 2
WHEN 86 THEN 3
WHEN 87 THEN 1
WHEN 88 THEN 2
WHEN 89 THEN 3
WHEN 90 THEN 1
WHEN 91 THEN 2
WHEN 92 THEN 3
WHEN 93 THEN 1
WHEN 94 THEN 2
WHEN 95 THEN 3
WHEN 96 THEN 1
WHEN 97 THEN 2
WHEN 98 THEN 3
WHEN 99 THEN 1
WHEN 100 THEN 2
ELSE primaryStoreId
END
WHERE id BETWEEN 15 AND 100;
UPDATE users
SET fullName = TRIM(CONCAT_WS(' ', firstName, lastName))
WHERE (fullName IS NULL OR fullName = '')
AND ((firstName IS NOT NULL AND firstName <> '') OR (lastName IS NOT NULL AND lastName <> ''));
UPDATE activityLog al
LEFT JOIN users u ON u.id = al.userId
LEFT JOIN storeLocation s ON s.storeId = al.storeId
SET al.usernameSnapshot = COALESCE(al.usernameSnapshot, u.username),
al.fullNameSnapshot = COALESCE(al.fullNameSnapshot, COALESCE(NULLIF(u.fullName, ''), TRIM(CONCAT_WS(' ', u.firstName, u.lastName)))),
al.roleSnapshot = COALESCE(al.roleSnapshot, u.role),
al.storeNameSnapshot = COALESCE(al.storeNameSnapshot, s.storeName)
WHERE al.usernameSnapshot IS NULL
OR al.fullNameSnapshot IS NULL
OR al.roleSnapshot IS NULL
OR (al.storeId IS NOT NULL AND al.storeNameSnapshot IS NULL);
UPDATE users
SET avatarUrl = REPLACE(avatarUrl, 'https://images.petshop.local/users/', '/uploads/avatars/')
WHERE avatarUrl LIKE 'https://images.petshop.local/users/%';
UPDATE pet
SET imageUrl = REPLACE(imageUrl, 'https://images.petshop.local/pets/', '/uploads/pets/')
WHERE imageUrl LIKE 'https://images.petshop.local/pets/%';
UPDATE product
SET imageUrl = REPLACE(imageUrl, 'https://images.petshop.local/products/', '/uploads/products/')
WHERE imageUrl LIKE 'https://images.petshop.local/products/%';
UPDATE storeLocation
SET imageUrl = REPLACE(imageUrl, 'https://images.petshop.local/stores/', '/stores/')
WHERE imageUrl LIKE 'https://images.petshop.local/stores/%';

View File

@@ -1,21 +0,0 @@
INSERT INTO users (username, password, email, firstName, lastName, fullName, phone, avatarUrl, role, staffRole, primaryStoreId, loyaltyPoints, active, tokenVersion)
SELECT 'ai.bot',
'$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq',
'bot@petshop.com',
'AI',
'Bot',
'AI Bot',
'000-000-0000',
'https://images.petshop.local/users/bot.webp',
'STAFF',
'CUSTOMER_SERVICE',
NULL,
0,
1,
0
FROM DUAL
WHERE NOT EXISTS (
SELECT 1
FROM users
WHERE username = 'ai.bot'
);

View File

@@ -1,7 +0,0 @@
ALTER TABLE activityLog
ADD COLUMN usernameSnapshot VARCHAR(50) NULL,
ADD COLUMN fullNameSnapshot VARCHAR(100) NULL,
ADD COLUMN roleSnapshot VARCHAR(20) NULL,
ADD COLUMN storeNameSnapshot VARCHAR(100) NULL;
CREATE INDEX idx_activity_log_timestamp_id ON activityLog(logTimestamp, logId);

View File

@@ -1,3 +0,0 @@
ALTER TABLE sale
ADD COLUMN pointsUsed INT NOT NULL DEFAULT 0,
ADD COLUMN pointsDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00;

View File

@@ -311,6 +311,10 @@ CREATE TABLE IF NOT EXISTS activityLog (
logId BIGINT AUTO_INCREMENT PRIMARY KEY, logId BIGINT AUTO_INCREMENT PRIMARY KEY,
userId BIGINT NOT NULL, userId BIGINT NOT NULL,
storeId BIGINT NULL, storeId BIGINT NULL,
usernameSnapshot VARCHAR(50) NULL,
fullNameSnapshot VARCHAR(100) NULL,
roleSnapshot VARCHAR(20) NULL,
storeNameSnapshot VARCHAR(100) NULL,
activity TEXT NOT NULL, activity TEXT NOT NULL,
logTimestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, logTimestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_activity_log_user FOREIGN KEY (userId) REFERENCES users(id), CONSTRAINT fk_activity_log_user FOREIGN KEY (userId) REFERENCES users(id),
@@ -343,3 +347,4 @@ CREATE INDEX idx_cart_user ON cart(userId);
CREATE INDEX idx_conversation_customer ON conversation(customerId); CREATE INDEX idx_conversation_customer ON conversation(customerId);
CREATE INDEX idx_conversation_staff ON conversation(staffId); CREATE INDEX idx_conversation_staff ON conversation(staffId);
CREATE INDEX idx_activity_log_store ON activityLog(storeId); CREATE INDEX idx_activity_log_store ON activityLog(storeId);
CREATE INDEX idx_activity_log_timestamp_id ON activityLog(logTimestamp, logId);

View File

@@ -69,91 +69,91 @@ INSERT INTO users (id, username, password, email, firstName, lastName, fullName,
(13, 'chloe.martin', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'chloe.martin@petshop.com', 'Chloe', 'Martin', 'Chloe Martin', '403-710-0013', 'https://images.petshop.local/users/013.webp', 'STAFF', 'VETERINARY_TECH', 3, 0, 1, 0), (13, 'chloe.martin', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'chloe.martin@petshop.com', 'Chloe', 'Martin', 'Chloe Martin', '403-710-0013', 'https://images.petshop.local/users/013.webp', 'STAFF', 'VETERINARY_TECH', 3, 0, 1, 0),
(14, 'owen.baker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'owen.baker@petshop.com', 'Owen', 'Baker', 'Owen Baker', '403-710-0014', 'https://images.petshop.local/users/014.webp', 'STAFF', 'GROOMER', 3, 0, 1, 0), (14, 'owen.baker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'owen.baker@petshop.com', 'Owen', 'Baker', 'Owen Baker', '403-710-0014', 'https://images.petshop.local/users/014.webp', 'STAFF', 'GROOMER', 3, 0, 1, 0),
(15, 'customer', '$2y$10$fgIlTHDYUOzvbczwdhQP7..YuAHr2cGODb9OBQJqole3AkiY4CGUq', 'customer@petshop.com', 'Test', 'Customer', 'Test Customer', '000-000-1002', 'https://images.petshop.local/users/015.webp', 'CUSTOMER', 'CUSTOMER', 1, 0, 1, 0), (15, 'customer', '$2y$10$fgIlTHDYUOzvbczwdhQP7..YuAHr2cGODb9OBQJqole3AkiY4CGUq', 'customer@petshop.com', 'Test', 'Customer', 'Test Customer', '000-000-1002', 'https://images.petshop.local/users/015.webp', 'CUSTOMER', 'CUSTOMER', 1, 0, 1, 0),
(16, 'alex.brown', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.brown@gmail.com', 'Alex', 'Brown', 'Alex Brown', '403-730-0016', 'https://images.petshop.local/users/016.webp', 'CUSTOMER', 'CUSTOMER', 2, 12, 1, 0), (16, 'maya.brown', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'maya.brown@gmail.com', 'Maya', 'Brown', 'Maya Brown', '403-730-0016', 'https://images.petshop.local/users/016.webp', 'CUSTOMER', 'CUSTOMER', 2, 12, 1, 0),
(17, 'alex.clark', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.clark@gmail.com', 'Alex', 'Clark', 'Alex Clark', '403-730-0017', 'https://images.petshop.local/users/017.webp', 'CUSTOMER', 'CUSTOMER', 3, 15, 1, 0), (17, 'noah.clark', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'noah.clark@gmail.com', 'Noah', 'Clark', 'Noah Clark', '403-730-0017', 'https://images.petshop.local/users/017.webp', 'CUSTOMER', 'CUSTOMER', 3, 15, 1, 0),
(18, 'alex.wilson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wilson@gmail.com', 'Alex', 'Wilson', 'Alex Wilson', '403-730-0018', 'https://images.petshop.local/users/018.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), (18, 'avery.wilson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'avery.wilson@gmail.com', 'Avery', 'Wilson', 'Avery Wilson', '403-730-0018', 'https://images.petshop.local/users/018.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0),
(19, 'alex.martinez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.martinez@gmail.com', 'Alex', 'Martinez', 'Alex Martinez', '403-730-0019', 'https://images.petshop.local/users/019.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0), (19, 'leah.martinez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'leah.martinez@gmail.com', 'Leah', 'Martinez', 'Leah Martinez', '403-730-0019', 'https://images.petshop.local/users/019.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0),
(20, 'alex.anderson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.anderson@gmail.com', 'Alex', 'Anderson', 'Alex Anderson', '403-730-0020', 'https://images.petshop.local/users/020.webp', 'CUSTOMER', 'CUSTOMER', 3, 12, 1, 0), (20, 'julian.anderson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'julian.anderson@gmail.com', 'Julian', 'Anderson', 'Julian Anderson', '403-730-0020', 'https://images.petshop.local/users/020.webp', 'CUSTOMER', 'CUSTOMER', 3, 12, 1, 0),
(21, 'alex.taylor', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.taylor@gmail.com', 'Alex', 'Taylor', 'Alex Taylor', '403-730-0021', 'https://images.petshop.local/users/021.webp', 'CUSTOMER', 'CUSTOMER', 1, 11, 1, 0), (21, 'zoe.taylor', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'zoe.taylor@gmail.com', 'Zoe', 'Taylor', 'Zoe Taylor', '403-730-0021', 'https://images.petshop.local/users/021.webp', 'CUSTOMER', 'CUSTOMER', 1, 11, 1, 0),
(22, 'alex.parker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.parker@gmail.com', 'Alex', 'Parker', 'Alex Parker', '403-730-0022', 'https://images.petshop.local/users/022.webp', 'CUSTOMER', 'CUSTOMER', 2, 16, 1, 0), (22, 'ethan.parker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'ethan.parker@gmail.com', 'Ethan', 'Parker', 'Ethan Parker', '403-730-0022', 'https://images.petshop.local/users/022.webp', 'CUSTOMER', 'CUSTOMER', 2, 16, 1, 0),
(23, 'alex.evans', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.evans@gmail.com', 'Alex', 'Evans', 'Alex Evans', '403-730-0023', 'https://images.petshop.local/users/023.webp', 'CUSTOMER', 'CUSTOMER', 3, 36, 1, 0), (23, 'ruby.evans', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'ruby.evans@gmail.com', 'Ruby', 'Evans', 'Ruby Evans', '403-730-0023', 'https://images.petshop.local/users/023.webp', 'CUSTOMER', 'CUSTOMER', 3, 36, 1, 0),
(24, 'alex.scott', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.scott@gmail.com', 'Alex', 'Scott', 'Alex Scott', '403-730-0024', 'https://images.petshop.local/users/024.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0), (24, 'caleb.scott', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'caleb.scott@gmail.com', 'Caleb', 'Scott', 'Caleb Scott', '403-730-0024', 'https://images.petshop.local/users/024.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0),
(25, 'alex.adams', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.adams@gmail.com', 'Alex', 'Adams', 'Alex Adams', '403-730-0025', 'https://images.petshop.local/users/025.webp', 'CUSTOMER', 'CUSTOMER', 2, 8, 1, 0), (25, 'ivy.adams', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'ivy.adams@gmail.com', 'Ivy', 'Adams', 'Ivy Adams', '403-730-0025', 'https://images.petshop.local/users/025.webp', 'CUSTOMER', 'CUSTOMER', 2, 8, 1, 0),
(26, 'alex.baker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.baker@gmail.com', 'Alex', 'Baker', 'Alex Baker', '403-730-0026', 'https://images.petshop.local/users/026.webp', 'CUSTOMER', 'CUSTOMER', 3, 29, 1, 0), (26, 'isaac.baker', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'isaac.baker@gmail.com', 'Isaac', 'Baker', 'Isaac Baker', '403-730-0026', 'https://images.petshop.local/users/026.webp', 'CUSTOMER', 'CUSTOMER', 3, 29, 1, 0),
(27, 'alex.hall', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hall@gmail.com', 'Alex', 'Hall', 'Alex Hall', '403-730-0027', 'https://images.petshop.local/users/027.webp', 'CUSTOMER', 'CUSTOMER', 1, 3, 1, 0), (27, 'hannah.hall', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'hannah.hall@gmail.com', 'Hannah', 'Hall', 'Hannah Hall', '403-730-0027', 'https://images.petshop.local/users/027.webp', 'CUSTOMER', 'CUSTOMER', 1, 3, 1, 0),
(28, 'alex.rivera', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.rivera@gmail.com', 'Alex', 'Rivera', 'Alex Rivera', '403-730-0028', 'https://images.petshop.local/users/028.webp', 'CUSTOMER', 'CUSTOMER', 2, 13, 1, 0), (28, 'mason.rivera', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'mason.rivera@gmail.com', 'Mason', 'Rivera', 'Mason Rivera', '403-730-0028', 'https://images.petshop.local/users/028.webp', 'CUSTOMER', 'CUSTOMER', 2, 13, 1, 0),
(29, 'alex.mitchell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.mitchell@gmail.com', 'Alex', 'Mitchell', 'Alex Mitchell', '403-730-0029', 'https://images.petshop.local/users/029.webp', 'CUSTOMER', 'CUSTOMER', 3, 30, 1, 0), (29, 'aria.mitchell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'aria.mitchell@gmail.com', 'Aria', 'Mitchell', 'Aria Mitchell', '403-730-0029', 'https://images.petshop.local/users/029.webp', 'CUSTOMER', 'CUSTOMER', 3, 30, 1, 0),
(30, 'alex.collins', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.collins@gmail.com', 'Alex', 'Collins', 'Alex Collins', '403-730-0030', 'https://images.petshop.local/users/030.webp', 'CUSTOMER', 'CUSTOMER', 1, 16, 1, 0), (30, 'wyatt.collins', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'wyatt.collins@gmail.com', 'Wyatt', 'Collins', 'Wyatt Collins', '403-730-0030', 'https://images.petshop.local/users/030.webp', 'CUSTOMER', 'CUSTOMER', 1, 16, 1, 0),
(31, 'alex.morris', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.morris@gmail.com', 'Alex', 'Morris', 'Alex Morris', '403-730-0031', 'https://images.petshop.local/users/031.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0), (31, 'elena.morris', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'elena.morris@gmail.com', 'Elena', 'Morris', 'Elena Morris', '403-730-0031', 'https://images.petshop.local/users/031.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0),
(32, 'alex.cook', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cook@gmail.com', 'Alex', 'Cook', 'Alex Cook', '403-730-0032', 'https://images.petshop.local/users/032.webp', 'CUSTOMER', 'CUSTOMER', 3, 19, 1, 0), (32, 'leo.cook', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'leo.cook@gmail.com', 'Leo', 'Cook', 'Leo Cook', '403-730-0032', 'https://images.petshop.local/users/032.webp', 'CUSTOMER', 'CUSTOMER', 3, 19, 1, 0),
(33, 'alex.bell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bell@gmail.com', 'Alex', 'Bell', 'Alex Bell', '403-730-0033', 'https://images.petshop.local/users/033.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), (33, 'grace.bell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'grace.bell@gmail.com', 'Grace', 'Bell', 'Grace Bell', '403-730-0033', 'https://images.petshop.local/users/033.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0),
(34, 'alex.reed', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.reed@gmail.com', 'Alex', 'Reed', 'Alex Reed', '403-730-0034', 'https://images.petshop.local/users/034.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0), (34, 'hudson.reed', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'hudson.reed@gmail.com', 'Hudson', 'Reed', 'Hudson Reed', '403-730-0034', 'https://images.petshop.local/users/034.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0),
(35, 'alex.murphy', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.murphy@gmail.com', 'Alex', 'Murphy', 'Alex Murphy', '403-730-0035', 'https://images.petshop.local/users/035.webp', 'CUSTOMER', 'CUSTOMER', 3, 31, 1, 0), (35, 'claire.murphy', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'claire.murphy@gmail.com', 'Claire', 'Murphy', 'Claire Murphy', '403-730-0035', 'https://images.petshop.local/users/035.webp', 'CUSTOMER', 'CUSTOMER', 3, 31, 1, 0),
(36, 'alex.bailey', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bailey@gmail.com', 'Alex', 'Bailey', 'Alex Bailey', '403-730-0036', 'https://images.petshop.local/users/036.webp', 'CUSTOMER', 'CUSTOMER', 1, 6, 1, 0), (36, 'omar.bailey', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'omar.bailey@gmail.com', 'Omar', 'Bailey', 'Omar Bailey', '403-730-0036', 'https://images.petshop.local/users/036.webp', 'CUSTOMER', 'CUSTOMER', 1, 6, 1, 0),
(37, 'alex.cooper', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cooper@gmail.com', 'Alex', 'Cooper', 'Alex Cooper', '403-730-0037', 'https://images.petshop.local/users/037.webp', 'CUSTOMER', 'CUSTOMER', 2, 4, 1, 0), (37, 'naomi.cooper', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'naomi.cooper@gmail.com', 'Naomi', 'Cooper', 'Naomi Cooper', '403-730-0037', 'https://images.petshop.local/users/037.webp', 'CUSTOMER', 'CUSTOMER', 2, 4, 1, 0),
(38, 'alex.richardson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.richardson@gmail.com', 'Alex', 'Richardson', 'Alex Richardson', '403-730-0038', 'https://images.petshop.local/users/038.webp', 'CUSTOMER', 'CUSTOMER', 3, 19, 1, 0), (38, 'jasper.richardson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'jasper.richardson@gmail.com', 'Jasper', 'Richardson', 'Jasper Richardson', '403-730-0038', 'https://images.petshop.local/users/038.webp', 'CUSTOMER', 'CUSTOMER', 3, 19, 1, 0),
(39, 'alex.cox', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cox@gmail.com', 'Alex', 'Cox', 'Alex Cox', '403-730-0039', 'https://images.petshop.local/users/039.webp', 'CUSTOMER', 'CUSTOMER', 1, 4, 1, 0), (39, 'sofia.cox', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'sofia.cox@gmail.com', 'Sofia', 'Cox', 'Sofia Cox', '403-730-0039', 'https://images.petshop.local/users/039.webp', 'CUSTOMER', 'CUSTOMER', 1, 4, 1, 0),
(40, 'alex.howard', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.howard@gmail.com', 'Alex', 'Howard', 'Alex Howard', '403-730-0040', 'https://images.petshop.local/users/040.webp', 'CUSTOMER', 'CUSTOMER', 2, 12, 1, 0), (40, 'miles.howard', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'miles.howard@gmail.com', 'Miles', 'Howard', 'Miles Howard', '403-730-0040', 'https://images.petshop.local/users/040.webp', 'CUSTOMER', 'CUSTOMER', 2, 12, 1, 0),
(41, 'alex.ward', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ward@gmail.com', 'Alex', 'Ward', 'Alex Ward', '403-730-0041', 'https://images.petshop.local/users/041.webp', 'CUSTOMER', 'CUSTOMER', 3, 18, 1, 0), (41, 'audrey.ward', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'audrey.ward@gmail.com', 'Audrey', 'Ward', 'Audrey Ward', '403-730-0041', 'https://images.petshop.local/users/041.webp', 'CUSTOMER', 'CUSTOMER', 3, 18, 1, 0),
(42, 'alex.torres', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.torres@gmail.com', 'Alex', 'Torres', 'Alex Torres', '403-730-0042', 'https://images.petshop.local/users/042.webp', 'CUSTOMER', 'CUSTOMER', 1, 10, 1, 0), (42, 'nathan.torres', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'nathan.torres@gmail.com', 'Nathan', 'Torres', 'Nathan Torres', '403-730-0042', 'https://images.petshop.local/users/042.webp', 'CUSTOMER', 'CUSTOMER', 1, 10, 1, 0),
(43, 'alex.peterson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.peterson@gmail.com', 'Alex', 'Peterson', 'Alex Peterson', '403-730-0043', 'https://images.petshop.local/users/043.webp', 'CUSTOMER', 'CUSTOMER', 2, 6, 1, 0), (43, 'jade.peterson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'jade.peterson@gmail.com', 'Jade', 'Peterson', 'Jade Peterson', '403-730-0043', 'https://images.petshop.local/users/043.webp', 'CUSTOMER', 'CUSTOMER', 2, 6, 1, 0),
(44, 'alex.gray', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gray@gmail.com', 'Alex', 'Gray', 'Alex Gray', '403-730-0044', 'https://images.petshop.local/users/044.webp', 'CUSTOMER', 'CUSTOMER', 3, 11, 1, 0), (44, 'rowan.gray', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'rowan.gray@gmail.com', 'Rowan', 'Gray', 'Rowan Gray', '403-730-0044', 'https://images.petshop.local/users/044.webp', 'CUSTOMER', 'CUSTOMER', 3, 11, 1, 0),
(45, 'alex.ramirez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ramirez@gmail.com', 'Alex', 'Ramirez', 'Alex Ramirez', '403-730-0045', 'https://images.petshop.local/users/045.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0), (45, 'lila.ramirez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'lila.ramirez@gmail.com', 'Lila', 'Ramirez', 'Lila Ramirez', '403-730-0045', 'https://images.petshop.local/users/045.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0),
(46, 'alex.james', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.james@gmail.com', 'Alex', 'James', 'Alex James', '403-730-0046', 'https://images.petshop.local/users/046.webp', 'CUSTOMER', 'CUSTOMER', 2, 28, 1, 0), (46, 'eli.james', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'eli.james@gmail.com', 'Eli', 'James', 'Eli James', '403-730-0046', 'https://images.petshop.local/users/046.webp', 'CUSTOMER', 'CUSTOMER', 2, 28, 1, 0),
(47, 'alex.watson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.watson@gmail.com', 'Alex', 'Watson', 'Alex Watson', '403-730-0047', 'https://images.petshop.local/users/047.webp', 'CUSTOMER', 'CUSTOMER', 3, 8, 1, 0), (47, 'violet.watson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'violet.watson@gmail.com', 'Violet', 'Watson', 'Violet Watson', '403-730-0047', 'https://images.petshop.local/users/047.webp', 'CUSTOMER', 'CUSTOMER', 3, 8, 1, 0),
(48, 'alex.brooks', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.brooks@gmail.com', 'Alex', 'Brooks', 'Alex Brooks', '403-730-0048', 'https://images.petshop.local/users/048.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), (48, 'gavin.brooks', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'gavin.brooks@gmail.com', 'Gavin', 'Brooks', 'Gavin Brooks', '403-730-0048', 'https://images.petshop.local/users/048.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0),
(49, 'alex.kelly', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.kelly@gmail.com', 'Alex', 'Kelly', 'Alex Kelly', '403-730-0049', 'https://images.petshop.local/users/049.webp', 'CUSTOMER', 'CUSTOMER', 2, 16, 1, 0), (49, 'stella.kelly', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'stella.kelly@gmail.com', 'Stella', 'Kelly', 'Stella Kelly', '403-730-0049', 'https://images.petshop.local/users/049.webp', 'CUSTOMER', 'CUSTOMER', 2, 16, 1, 0),
(50, 'alex.sanders', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.sanders@gmail.com', 'Alex', 'Sanders', 'Alex Sanders', '403-730-0050', 'https://images.petshop.local/users/050.webp', 'CUSTOMER', 'CUSTOMER', 3, 21, 1, 0), (50, 'adrian.sanders', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'adrian.sanders@gmail.com', 'Adrian', 'Sanders', 'Adrian Sanders', '403-730-0050', 'https://images.petshop.local/users/050.webp', 'CUSTOMER', 'CUSTOMER', 3, 21, 1, 0),
(51, 'alex.price', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.price@gmail.com', 'Alex', 'Price', 'Alex Price', '403-730-0051', 'https://images.petshop.local/users/051.webp', 'CUSTOMER', 'CUSTOMER', 1, 7, 1, 0), (51, 'hazel.price', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'hazel.price@gmail.com', 'Hazel', 'Price', 'Hazel Price', '403-730-0051', 'https://images.petshop.local/users/051.webp', 'CUSTOMER', 'CUSTOMER', 1, 7, 1, 0),
(52, 'alex.bennett', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bennett@gmail.com', 'Alex', 'Bennett', 'Alex Bennett', '403-730-0052', 'https://images.petshop.local/users/052.webp', 'CUSTOMER', 'CUSTOMER', 2, 17, 1, 0), (52, 'connor.bennett', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'connor.bennett@gmail.com', 'Connor', 'Bennett', 'Connor Bennett', '403-730-0052', 'https://images.petshop.local/users/052.webp', 'CUSTOMER', 'CUSTOMER', 2, 17, 1, 0),
(53, 'alex.wood', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wood@gmail.com', 'Alex', 'Wood', 'Alex Wood', '403-730-0053', 'https://images.petshop.local/users/053.webp', 'CUSTOMER', 'CUSTOMER', 3, 10, 1, 0), (53, 'sadie.wood', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'sadie.wood@gmail.com', 'Sadie', 'Wood', 'Sadie Wood', '403-730-0053', 'https://images.petshop.local/users/053.webp', 'CUSTOMER', 'CUSTOMER', 3, 10, 1, 0),
(54, 'alex.barnes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.barnes@gmail.com', 'Alex', 'Barnes', 'Alex Barnes', '403-730-0054', 'https://images.petshop.local/users/054.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), (54, 'xavier.barnes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'xavier.barnes@gmail.com', 'Xavier', 'Barnes', 'Xavier Barnes', '403-730-0054', 'https://images.petshop.local/users/054.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0),
(55, 'alex.ross', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ross@gmail.com', 'Alex', 'Ross', 'Alex Ross', '403-730-0055', 'https://images.petshop.local/users/055.webp', 'CUSTOMER', 'CUSTOMER', 2, 7, 1, 0), (55, 'alice.ross', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alice.ross@gmail.com', 'Alice', 'Ross', 'Alice Ross', '403-730-0055', 'https://images.petshop.local/users/055.webp', 'CUSTOMER', 'CUSTOMER', 2, 7, 1, 0),
(56, 'alex.henderson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.henderson@gmail.com', 'Alex', 'Henderson', 'Alex Henderson', '403-730-0056', 'https://images.petshop.local/users/056.webp', 'CUSTOMER', 'CUSTOMER', 3, 15, 1, 0), (56, 'roman.henderson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'roman.henderson@gmail.com', 'Roman', 'Henderson', 'Roman Henderson', '403-730-0056', 'https://images.petshop.local/users/056.webp', 'CUSTOMER', 'CUSTOMER', 3, 15, 1, 0),
(57, 'alex.coleman', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.coleman@gmail.com', 'Alex', 'Coleman', 'Alex Coleman', '403-730-0057', 'https://images.petshop.local/users/057.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), (57, 'lucy.coleman', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'lucy.coleman@gmail.com', 'Lucy', 'Coleman', 'Lucy Coleman', '403-730-0057', 'https://images.petshop.local/users/057.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0),
(58, 'alex.jenkins', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.jenkins@gmail.com', 'Alex', 'Jenkins', 'Alex Jenkins', '403-730-0058', 'https://images.petshop.local/users/058.webp', 'CUSTOMER', 'CUSTOMER', 2, 17, 1, 0), (58, 'evan.jenkins', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'evan.jenkins@gmail.com', 'Evan', 'Jenkins', 'Evan Jenkins', '403-730-0058', 'https://images.petshop.local/users/058.webp', 'CUSTOMER', 'CUSTOMER', 2, 17, 1, 0),
(59, 'alex.perry', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.perry@gmail.com', 'Alex', 'Perry', 'Alex Perry', '403-730-0059', 'https://images.petshop.local/users/059.webp', 'CUSTOMER', 'CUSTOMER', 3, 15, 1, 0), (59, 'mila.perry', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'mila.perry@gmail.com', 'Mila', 'Perry', 'Mila Perry', '403-730-0059', 'https://images.petshop.local/users/059.webp', 'CUSTOMER', 'CUSTOMER', 3, 15, 1, 0),
(60, 'alex.powell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.powell@gmail.com', 'Alex', 'Powell', 'Alex Powell', '403-730-0060', 'https://images.petshop.local/users/060.webp', 'CUSTOMER', 'CUSTOMER', 1, 4, 1, 0), (60, 'cole.powell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'cole.powell@gmail.com', 'Cole', 'Powell', 'Cole Powell', '403-730-0060', 'https://images.petshop.local/users/060.webp', 'CUSTOMER', 'CUSTOMER', 1, 4, 1, 0),
(61, 'alex.long', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.long@gmail.com', 'Alex', 'Long', 'Alex Long', '403-730-0061', 'https://images.petshop.local/users/061.webp', 'CUSTOMER', 'CUSTOMER', 2, 13, 1, 0), (61, 'nora.long', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'nora.long@gmail.com', 'Nora', 'Long', 'Nora Long', '403-730-0061', 'https://images.petshop.local/users/061.webp', 'CUSTOMER', 'CUSTOMER', 2, 13, 1, 0),
(62, 'alex.patterson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.patterson@gmail.com', 'Alex', 'Patterson', 'Alex Patterson', '403-730-0062', 'https://images.petshop.local/users/062.webp', 'CUSTOMER', 'CUSTOMER', 3, 26, 1, 0), (62, 'adam.patterson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'adam.patterson@gmail.com', 'Adam', 'Patterson', 'Adam Patterson', '403-730-0062', 'https://images.petshop.local/users/062.webp', 'CUSTOMER', 'CUSTOMER', 3, 26, 1, 0),
(63, 'alex.hughes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hughes@gmail.com', 'Alex', 'Hughes', 'Alex Hughes', '403-730-0063', 'https://images.petshop.local/users/063.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0), (63, 'layla.hughes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'layla.hughes@gmail.com', 'Layla', 'Hughes', 'Layla Hughes', '403-730-0063', 'https://images.petshop.local/users/063.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0),
(64, 'alex.flores', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.flores@gmail.com', 'Alex', 'Flores', 'Alex Flores', '403-730-0064', 'https://images.petshop.local/users/064.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0), (64, 'blake.flores', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'blake.flores@gmail.com', 'Blake', 'Flores', 'Blake Flores', '403-730-0064', 'https://images.petshop.local/users/064.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0),
(65, 'alex.washington', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.washington@gmail.com', 'Alex', 'Washington', 'Alex Washington', '403-730-0065', 'https://images.petshop.local/users/065.webp', 'CUSTOMER', 'CUSTOMER', 3, 22, 1, 0), (65, 'ellie.washington', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'ellie.washington@gmail.com', 'Ellie', 'Washington', 'Ellie Washington', '403-730-0065', 'https://images.petshop.local/users/065.webp', 'CUSTOMER', 'CUSTOMER', 3, 22, 1, 0),
(66, 'alex.butler', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.butler@gmail.com', 'Alex', 'Butler', 'Alex Butler', '403-730-0066', 'https://images.petshop.local/users/066.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0), (66, 'ryan.butler', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'ryan.butler@gmail.com', 'Ryan', 'Butler', 'Ryan Butler', '403-730-0066', 'https://images.petshop.local/users/066.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0),
(67, 'alex.simmons', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.simmons@gmail.com', 'Alex', 'Simmons', 'Alex Simmons', '403-730-0067', 'https://images.petshop.local/users/067.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0), (67, 'cora.simmons', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'cora.simmons@gmail.com', 'Cora', 'Simmons', 'Cora Simmons', '403-730-0067', 'https://images.petshop.local/users/067.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0),
(68, 'alex.foster', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.foster@gmail.com', 'Alex', 'Foster', 'Alex Foster', '403-730-0068', 'https://images.petshop.local/users/068.webp', 'CUSTOMER', 'CUSTOMER', 3, 17, 1, 0), (68, 'simon.foster', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'simon.foster@gmail.com', 'Simon', 'Foster', 'Simon Foster', '403-730-0068', 'https://images.petshop.local/users/068.webp', 'CUSTOMER', 'CUSTOMER', 3, 17, 1, 0),
(69, 'alex.gonzales', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gonzales@gmail.com', 'Alex', 'Gonzales', 'Alex Gonzales', '403-730-0069', 'https://images.petshop.local/users/069.webp', 'CUSTOMER', 'CUSTOMER', 1, 15, 1, 0), (69, 'piper.gonzales', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'piper.gonzales@gmail.com', 'Piper', 'Gonzales', 'Piper Gonzales', '403-730-0069', 'https://images.petshop.local/users/069.webp', 'CUSTOMER', 'CUSTOMER', 1, 15, 1, 0),
(70, 'alex.bryant', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.bryant@gmail.com', 'Alex', 'Bryant', 'Alex Bryant', '403-730-0070', 'https://images.petshop.local/users/070.webp', 'CUSTOMER', 'CUSTOMER', 2, 19, 1, 0), (70, 'joel.bryant', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'joel.bryant@gmail.com', 'Joel', 'Bryant', 'Joel Bryant', '403-730-0070', 'https://images.petshop.local/users/070.webp', 'CUSTOMER', 'CUSTOMER', 2, 19, 1, 0),
(71, 'alex.alexander', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.alexander@gmail.com', 'Alex', 'Alexander', 'Alex Alexander', '403-730-0071', 'https://images.petshop.local/users/071.webp', 'CUSTOMER', 'CUSTOMER', 3, 13, 1, 0), (71, 'eva.alexander', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'eva.alexander@gmail.com', 'Eva', 'Alexander', 'Eva Alexander', '403-730-0071', 'https://images.petshop.local/users/071.webp', 'CUSTOMER', 'CUSTOMER', 3, 13, 1, 0),
(72, 'alex.russell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.russell@gmail.com', 'Alex', 'Russell', 'Alex Russell', '403-730-0072', 'https://images.petshop.local/users/072.webp', 'CUSTOMER', 'CUSTOMER', 1, 7, 1, 0), (72, 'felix.russell', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'felix.russell@gmail.com', 'Felix', 'Russell', 'Felix Russell', '403-730-0072', 'https://images.petshop.local/users/072.webp', 'CUSTOMER', 'CUSTOMER', 1, 7, 1, 0),
(73, 'alex.griffin', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.griffin@gmail.com', 'Alex', 'Griffin', 'Alex Griffin', '403-730-0073', 'https://images.petshop.local/users/073.webp', 'CUSTOMER', 'CUSTOMER', 2, 2, 1, 0), (73, 'maeve.griffin', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'maeve.griffin@gmail.com', 'Maeve', 'Griffin', 'Maeve Griffin', '403-730-0073', 'https://images.petshop.local/users/073.webp', 'CUSTOMER', 'CUSTOMER', 2, 2, 1, 0),
(74, 'alex.diaz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.diaz@gmail.com', 'Alex', 'Diaz', 'Alex Diaz', '403-730-0074', 'https://images.petshop.local/users/074.webp', 'CUSTOMER', 'CUSTOMER', 3, 10, 1, 0), (74, 'tristan.diaz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'tristan.diaz@gmail.com', 'Tristan', 'Diaz', 'Tristan Diaz', '403-730-0074', 'https://images.petshop.local/users/074.webp', 'CUSTOMER', 'CUSTOMER', 3, 10, 1, 0),
(75, 'alex.hayes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hayes@gmail.com', 'Alex', 'Hayes', 'Alex Hayes', '403-730-0075', 'https://images.petshop.local/users/075.webp', 'CUSTOMER', 'CUSTOMER', 1, 7, 1, 0), (75, 'ariana.hayes', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'ariana.hayes@gmail.com', 'Ariana', 'Hayes', 'Ariana Hayes', '403-730-0075', 'https://images.petshop.local/users/075.webp', 'CUSTOMER', 'CUSTOMER', 1, 7, 1, 0),
(76, 'alex.myers', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.myers@gmail.com', 'Alex', 'Myers', 'Alex Myers', '403-730-0076', 'https://images.petshop.local/users/076.webp', 'CUSTOMER', 'CUSTOMER', 2, 13, 1, 0), (76, 'declan.myers', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'declan.myers@gmail.com', 'Declan', 'Myers', 'Declan Myers', '403-730-0076', 'https://images.petshop.local/users/076.webp', 'CUSTOMER', 'CUSTOMER', 2, 13, 1, 0),
(77, 'alex.ford', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ford@gmail.com', 'Alex', 'Ford', 'Alex Ford', '403-730-0077', 'https://images.petshop.local/users/077.webp', 'CUSTOMER', 'CUSTOMER', 3, 13, 1, 0), (77, 'brooke.ford', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'brooke.ford@gmail.com', 'Brooke', 'Ford', 'Brooke Ford', '403-730-0077', 'https://images.petshop.local/users/077.webp', 'CUSTOMER', 'CUSTOMER', 3, 13, 1, 0),
(78, 'alex.hamilton', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.hamilton@gmail.com', 'Alex', 'Hamilton', 'Alex Hamilton', '403-730-0078', 'https://images.petshop.local/users/078.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), (78, 'micah.hamilton', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'micah.hamilton@gmail.com', 'Micah', 'Hamilton', 'Micah Hamilton', '403-730-0078', 'https://images.petshop.local/users/078.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0),
(79, 'alex.graham', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.graham@gmail.com', 'Alex', 'Graham', 'Alex Graham', '403-730-0079', 'https://images.petshop.local/users/079.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0), (79, 'bianca.graham', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'bianca.graham@gmail.com', 'Bianca', 'Graham', 'Bianca Graham', '403-730-0079', 'https://images.petshop.local/users/079.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0),
(80, 'alex.sullivan', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.sullivan@gmail.com', 'Alex', 'Sullivan', 'Alex Sullivan', '403-730-0080', 'https://images.petshop.local/users/080.webp', 'CUSTOMER', 'CUSTOMER', 3, 12, 1, 0), (80, 'jonah.sullivan', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'jonah.sullivan@gmail.com', 'Jonah', 'Sullivan', 'Jonah Sullivan', '403-730-0080', 'https://images.petshop.local/users/080.webp', 'CUSTOMER', 'CUSTOMER', 3, 12, 1, 0),
(81, 'alex.wallace', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wallace@gmail.com', 'Alex', 'Wallace', 'Alex Wallace', '403-730-0081', 'https://images.petshop.local/users/081.webp', 'CUSTOMER', 'CUSTOMER', 1, 11, 1, 0), (81, 'tessa.wallace', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'tessa.wallace@gmail.com', 'Tessa', 'Wallace', 'Tessa Wallace', '403-730-0081', 'https://images.petshop.local/users/081.webp', 'CUSTOMER', 'CUSTOMER', 1, 11, 1, 0),
(82, 'alex.woods', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.woods@gmail.com', 'Alex', 'Woods', 'Alex Woods', '403-730-0082', 'https://images.petshop.local/users/082.webp', 'CUSTOMER', 'CUSTOMER', 2, 17, 1, 0), (82, 'damian.woods', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'damian.woods@gmail.com', 'Damian', 'Woods', 'Damian Woods', '403-730-0082', 'https://images.petshop.local/users/082.webp', 'CUSTOMER', 'CUSTOMER', 2, 17, 1, 0),
(83, 'alex.cole', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cole@gmail.com', 'Alex', 'Cole', 'Alex Cole', '403-730-0083', 'https://images.petshop.local/users/083.webp', 'CUSTOMER', 'CUSTOMER', 3, 36, 1, 0), (83, 'riley.cole', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'riley.cole@gmail.com', 'Riley', 'Cole', 'Riley Cole', '403-730-0083', 'https://images.petshop.local/users/083.webp', 'CUSTOMER', 'CUSTOMER', 3, 36, 1, 0),
(84, 'alex.west', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.west@gmail.com', 'Alex', 'West', 'Alex West', '403-730-0084', 'https://images.petshop.local/users/084.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0), (84, 'kieran.west', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'kieran.west@gmail.com', 'Kieran', 'West', 'Kieran West', '403-730-0084', 'https://images.petshop.local/users/084.webp', 'CUSTOMER', 'CUSTOMER', 1, 5, 1, 0),
(85, 'alex.jordan', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.jordan@gmail.com', 'Alex', 'Jordan', 'Alex Jordan', '403-730-0085', 'https://images.petshop.local/users/085.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0), (85, 'sienna.jordan', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'sienna.jordan@gmail.com', 'Sienna', 'Jordan', 'Sienna Jordan', '403-730-0085', 'https://images.petshop.local/users/085.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0),
(86, 'alex.owens', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.owens@gmail.com', 'Alex', 'Owens', 'Alex Owens', '403-730-0086', 'https://images.petshop.local/users/086.webp', 'CUSTOMER', 'CUSTOMER', 3, 26, 1, 0), (86, 'finley.owens', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'finley.owens@gmail.com', 'Finley', 'Owens', 'Finley Owens', '403-730-0086', 'https://images.petshop.local/users/086.webp', 'CUSTOMER', 'CUSTOMER', 3, 26, 1, 0),
(87, 'alex.reynolds', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.reynolds@gmail.com', 'Alex', 'Reynolds', 'Alex Reynolds', '403-730-0087', 'https://images.petshop.local/users/087.webp', 'CUSTOMER', 'CUSTOMER', 1, 3, 1, 0), (87, 'maren.reynolds', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'maren.reynolds@gmail.com', 'Maren', 'Reynolds', 'Maren Reynolds', '403-730-0087', 'https://images.petshop.local/users/087.webp', 'CUSTOMER', 'CUSTOMER', 1, 3, 1, 0),
(88, 'alex.fisher', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.fisher@gmail.com', 'Alex', 'Fisher', 'Alex Fisher', '403-730-0088', 'https://images.petshop.local/users/088.webp', 'CUSTOMER', 'CUSTOMER', 2, 11, 1, 0), (88, 'asher.fisher', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'asher.fisher@gmail.com', 'Asher', 'Fisher', 'Asher Fisher', '403-730-0088', 'https://images.petshop.local/users/088.webp', 'CUSTOMER', 'CUSTOMER', 2, 11, 1, 0),
(89, 'alex.ellis', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ellis@gmail.com', 'Alex', 'Ellis', 'Alex Ellis', '403-730-0089', 'https://images.petshop.local/users/089.webp', 'CUSTOMER', 'CUSTOMER', 3, 30, 1, 0), (89, 'daphne.ellis', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'daphne.ellis@gmail.com', 'Daphne', 'Ellis', 'Daphne Ellis', '403-730-0089', 'https://images.petshop.local/users/089.webp', 'CUSTOMER', 'CUSTOMER', 3, 30, 1, 0),
(90, 'alex.harrison', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.harrison@gmail.com', 'Alex', 'Harrison', 'Alex Harrison', '403-730-0090', 'https://images.petshop.local/users/090.webp', 'CUSTOMER', 'CUSTOMER', 1, 16, 1, 0), (90, 'bennett.harrison', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'bennett.harrison@gmail.com', 'Bennett', 'Harrison', 'Bennett Harrison', '403-730-0090', 'https://images.petshop.local/users/090.webp', 'CUSTOMER', 'CUSTOMER', 1, 16, 1, 0),
(91, 'alex.gibson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gibson@gmail.com', 'Alex', 'Gibson', 'Alex Gibson', '403-730-0091', 'https://images.petshop.local/users/091.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0), (91, 'selena.gibson', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'selena.gibson@gmail.com', 'Selena', 'Gibson', 'Selena Gibson', '403-730-0091', 'https://images.petshop.local/users/091.webp', 'CUSTOMER', 'CUSTOMER', 2, 9, 1, 0),
(92, 'alex.mcdonald', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.mcdonald@gmail.com', 'Alex', 'Mcdonald', 'Alex Mcdonald', '403-730-0092', 'https://images.petshop.local/users/092.webp', 'CUSTOMER', 'CUSTOMER', 3, 19, 1, 0), (92, 'emmett.mcdonald', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'emmett.mcdonald@gmail.com', 'Emmett', 'Mcdonald', 'Emmett Mcdonald', '403-730-0092', 'https://images.petshop.local/users/092.webp', 'CUSTOMER', 'CUSTOMER', 3, 19, 1, 0),
(93, 'alex.cruz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.cruz@gmail.com', 'Alex', 'Cruz', 'Alex Cruz', '403-730-0093', 'https://images.petshop.local/users/093.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0), (93, 'phoebe.cruz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'phoebe.cruz@gmail.com', 'Phoebe', 'Cruz', 'Phoebe Cruz', '403-730-0093', 'https://images.petshop.local/users/093.webp', 'CUSTOMER', 'CUSTOMER', 1, 2, 1, 0),
(94, 'alex.marshall', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.marshall@gmail.com', 'Alex', 'Marshall', 'Alex Marshall', '403-730-0094', 'https://images.petshop.local/users/094.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0), (94, 'sawyer.marshall', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'sawyer.marshall@gmail.com', 'Sawyer', 'Marshall', 'Sawyer Marshall', '403-730-0094', 'https://images.petshop.local/users/094.webp', 'CUSTOMER', 'CUSTOMER', 2, 5, 1, 0),
(95, 'alex.ortiz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.ortiz@gmail.com', 'Alex', 'Ortiz', 'Alex Ortiz', '403-730-0095', 'https://images.petshop.local/users/095.webp', 'CUSTOMER', 'CUSTOMER', 3, 30, 1, 0), (95, 'keira.ortiz', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'keira.ortiz@gmail.com', 'Keira', 'Ortiz', 'Keira Ortiz', '403-730-0095', 'https://images.petshop.local/users/095.webp', 'CUSTOMER', 'CUSTOMER', 3, 30, 1, 0),
(96, 'alex.gomez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.gomez@gmail.com', 'Alex', 'Gomez', 'Alex Gomez', '403-730-0096', 'https://images.petshop.local/users/096.webp', 'CUSTOMER', 'CUSTOMER', 1, 6, 1, 0), (96, 'landon.gomez', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'landon.gomez@gmail.com', 'Landon', 'Gomez', 'Landon Gomez', '403-730-0096', 'https://images.petshop.local/users/096.webp', 'CUSTOMER', 'CUSTOMER', 1, 6, 1, 0),
(97, 'alex.murray', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.murray@gmail.com', 'Alex', 'Murray', 'Alex Murray', '403-730-0097', 'https://images.petshop.local/users/097.webp', 'CUSTOMER', 'CUSTOMER', 2, 4, 1, 0), (97, 'rosalie.murray', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'rosalie.murray@gmail.com', 'Rosalie', 'Murray', 'Rosalie Murray', '403-730-0097', 'https://images.petshop.local/users/097.webp', 'CUSTOMER', 'CUSTOMER', 2, 4, 1, 0),
(98, 'alex.freeman', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.freeman@gmail.com', 'Alex', 'Freeman', 'Alex Freeman', '403-730-0098', 'https://images.petshop.local/users/098.webp', 'CUSTOMER', 'CUSTOMER', 3, 0, 1, 0), (98, 'malik.freeman', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'malik.freeman@gmail.com', 'Malik', 'Freeman', 'Malik Freeman', '403-730-0098', 'https://images.petshop.local/users/098.webp', 'CUSTOMER', 'CUSTOMER', 3, 0, 1, 0),
(99, 'alex.wells', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.wells@gmail.com', 'Alex', 'Wells', 'Alex Wells', '403-730-0099', 'https://images.petshop.local/users/099.webp', 'CUSTOMER', 'CUSTOMER', 1, 0, 1, 0), (99, 'esme.wells', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'esme.wells@gmail.com', 'Esme', 'Wells', 'Esme Wells', '403-730-0099', 'https://images.petshop.local/users/099.webp', 'CUSTOMER', 'CUSTOMER', 1, 0, 1, 0),
(100, 'alex.webb', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'alex.webb@gmail.com', 'Alex', 'Webb', 'Alex Webb', '403-730-0100', 'https://images.petshop.local/users/100.webp', 'CUSTOMER', 'CUSTOMER', 2, 0, 1, 0), (100, 'holden.webb', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'holden.webb@gmail.com', 'Holden', 'Webb', 'Holden Webb', '403-730-0100', 'https://images.petshop.local/users/100.webp', 'CUSTOMER', 'CUSTOMER', 2, 0, 1, 0),
(101, 'ai.bot', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'bot@petshop.com', 'AI', 'Bot', 'AI Bot', '000-000-0000', 'https://images.petshop.local/users/bot.webp', 'STAFF', 'CUSTOMER_SERVICE', NULL, 0, 1, 0); (101, 'ai.bot', '$2a$10$mE0D/HrnCuqFeEqMy0NJwuy2jkoRYjQ7GrKcc/7QQ0r2AqnZTvyGq', 'bot@petshop.com', 'AI', 'Bot', 'AI Bot', '000-000-0000', 'https://images.petshop.local/users/bot.webp', 'STAFF', 'CUSTOMER_SERVICE', NULL, 0, 1, 0);
INSERT INTO supplier (supId, supCompany, supContactFirstName, supContactLastName, supEmail, supPhone) VALUES INSERT INTO supplier (supId, supCompany, supContactFirstName, supContactLastName, supEmail, supPhone) VALUES
@@ -1739,124 +1739,124 @@ INSERT INTO message (id, conversationId, senderId, content, attachmentUrl, attac
(119, 30, 45, 'Order #1030 is the one I meant, and the pet is Kiki.', NULL, NULL, NULL, NULL, '2026-03-02 09:10:00', 1), (119, 30, 45, 'Order #1030 is the one I meant, and the pet is Kiki.', NULL, NULL, NULL, NULL, '2026-03-02 09:10:00', 1),
(120, 30, 8, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-03-02 09:15:00', 0); (120, 30, 8, 'Thanks, the account and order are now updated on this conversation.', NULL, NULL, NULL, NULL, '2026-03-02 09:15:00', 0);
INSERT INTO activityLog (logId, userId, storeId, activity, logTimestamp) VALUES INSERT INTO activityLog (logId, userId, storeId, usernameSnapshot, fullNameSnapshot, roleSnapshot, storeNameSnapshot, activity, logTimestamp) VALUES
(1, 1, 1, 'Reviewed store inventory adjustments.', '2026-01-03 08:00:00'), (1, 1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Reviewed store inventory adjustments.', '2026-01-03 08:00:00'),
(2, 2, 2, 'Approved a purchase transaction at the register.', '2026-01-03 17:00:00'), (2, 2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Approved a purchase transaction at the register.', '2026-01-03 17:00:00'),
(3, 3, 1, 'Updated a pet availability record.', '2026-01-04 02:00:00'), (3, 3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Updated a pet availability record.', '2026-01-04 02:00:00'),
(4, 4, 1, 'Completed a grooming appointment handoff.', '2026-01-04 11:00:00'), (4, 4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Completed a grooming appointment handoff.', '2026-01-04 11:00:00'),
(5, 5, 1, 'Checked a pending adoption record.', '2026-01-04 20:00:00'), (5, 5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Checked a pending adoption record.', '2026-01-04 20:00:00'),
(6, 6, 1, 'Reviewed a refund request tied to an original sale.', '2026-01-05 05:00:00'), (6, 6, 1, 'priya.patel', 'Priya Patel', 'STAFF', 'Downtown Branch', 'Reviewed a refund request tied to an original sale.', '2026-01-05 05:00:00'),
(7, 7, 2, 'Answered a customer support conversation.', '2026-01-05 14:00:00'), (7, 7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Answered a customer support conversation.', '2026-01-05 14:00:00'),
(8, 8, 2, 'Updated a product detail for the catalogue.', '2026-01-05 23:00:00'), (8, 8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Updated a product detail for the catalogue.', '2026-01-05 23:00:00'),
(9, 9, 2, 'Reviewed store inventory adjustments.', '2026-01-06 08:00:00'), (9, 9, 2, 'lucas.turner', 'Lucas Turner', 'STAFF', 'North Branch', 'Reviewed store inventory adjustments.', '2026-01-06 08:00:00'),
(10, 10, 2, 'Approved a purchase transaction at the register.', '2026-01-06 17:00:00'), (10, 10, 2, 'nina.green', 'Nina Green', 'STAFF', 'North Branch', 'Approved a purchase transaction at the register.', '2026-01-06 17:00:00'),
(11, 11, 3, 'Updated a pet availability record.', '2026-01-07 02:00:00'), (11, 11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Updated a pet availability record.', '2026-01-07 02:00:00'),
(12, 12, 3, 'Completed a grooming appointment handoff.', '2026-01-07 11:00:00'), (12, 12, 3, 'daniel.moore', 'Daniel Moore', 'STAFF', 'West Side Store', 'Completed a grooming appointment handoff.', '2026-01-07 11:00:00'),
(13, 13, 3, 'Checked a pending adoption record.', '2026-01-07 20:00:00'), (13, 13, 3, 'chloe.martin', 'Chloe Martin', 'STAFF', 'West Side Store', 'Checked a pending adoption record.', '2026-01-07 20:00:00'),
(14, 14, 3, 'Reviewed a refund request tied to an original sale.', '2026-01-08 05:00:00'), (14, 14, 3, 'owen.baker', 'Owen Baker', 'STAFF', 'West Side Store', 'Reviewed a refund request tied to an original sale.', '2026-01-08 05:00:00'),
(15, 1, 1, 'Answered a customer support conversation.', '2026-01-08 14:00:00'), (15, 1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Answered a customer support conversation.', '2026-01-08 14:00:00'),
(16, 2, 2, 'Updated a product detail for the catalogue.', '2026-01-08 23:00:00'), (16, 2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Updated a product detail for the catalogue.', '2026-01-08 23:00:00'),
(17, 3, 1, 'Reviewed store inventory adjustments.', '2026-01-09 08:00:00'), (17, 3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Reviewed store inventory adjustments.', '2026-01-09 08:00:00'),
(18, 4, 1, 'Approved a purchase transaction at the register.', '2026-01-09 17:00:00'), (18, 4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Approved a purchase transaction at the register.', '2026-01-09 17:00:00'),
(19, 5, 1, 'Updated a pet availability record.', '2026-01-10 02:00:00'), (19, 5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Updated a pet availability record.', '2026-01-10 02:00:00'),
(20, 6, 1, 'Completed a grooming appointment handoff.', '2026-01-10 11:00:00'), (20, 6, 1, 'priya.patel', 'Priya Patel', 'STAFF', 'Downtown Branch', 'Completed a grooming appointment handoff.', '2026-01-10 11:00:00'),
(21, 7, 2, 'Checked a pending adoption record.', '2026-01-10 20:00:00'), (21, 7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Checked a pending adoption record.', '2026-01-10 20:00:00'),
(22, 8, 2, 'Reviewed a refund request tied to an original sale.', '2026-01-11 05:00:00'), (22, 8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Reviewed a refund request tied to an original sale.', '2026-01-11 05:00:00'),
(23, 9, 2, 'Answered a customer support conversation.', '2026-01-11 14:00:00'), (23, 9, 2, 'lucas.turner', 'Lucas Turner', 'STAFF', 'North Branch', 'Answered a customer support conversation.', '2026-01-11 14:00:00'),
(24, 10, 2, 'Updated a product detail for the catalogue.', '2026-01-11 23:00:00'), (24, 10, 2, 'nina.green', 'Nina Green', 'STAFF', 'North Branch', 'Updated a product detail for the catalogue.', '2026-01-11 23:00:00'),
(25, 11, 3, 'Reviewed store inventory adjustments.', '2026-01-12 08:00:00'), (25, 11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Reviewed store inventory adjustments.', '2026-01-12 08:00:00'),
(26, 12, 3, 'Approved a purchase transaction at the register.', '2026-01-12 17:00:00'), (26, 12, 3, 'daniel.moore', 'Daniel Moore', 'STAFF', 'West Side Store', 'Approved a purchase transaction at the register.', '2026-01-12 17:00:00'),
(27, 13, 3, 'Updated a pet availability record.', '2026-01-13 02:00:00'), (27, 13, 3, 'chloe.martin', 'Chloe Martin', 'STAFF', 'West Side Store', 'Updated a pet availability record.', '2026-01-13 02:00:00'),
(28, 14, 3, 'Completed a grooming appointment handoff.', '2026-01-13 11:00:00'), (28, 14, 3, 'owen.baker', 'Owen Baker', 'STAFF', 'West Side Store', 'Completed a grooming appointment handoff.', '2026-01-13 11:00:00'),
(29, 1, 1, 'Checked a pending adoption record.', '2026-01-13 20:00:00'), (29, 1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Checked a pending adoption record.', '2026-01-13 20:00:00'),
(30, 2, 2, 'Reviewed a refund request tied to an original sale.', '2026-01-14 05:00:00'), (30, 2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Reviewed a refund request tied to an original sale.', '2026-01-14 05:00:00'),
(31, 3, 1, 'Answered a customer support conversation.', '2026-01-14 14:00:00'), (31, 3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Answered a customer support conversation.', '2026-01-14 14:00:00'),
(32, 4, 1, 'Updated a product detail for the catalogue.', '2026-01-14 23:00:00'), (32, 4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Updated a product detail for the catalogue.', '2026-01-14 23:00:00'),
(33, 5, 1, 'Reviewed store inventory adjustments.', '2026-01-15 08:00:00'), (33, 5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Reviewed store inventory adjustments.', '2026-01-15 08:00:00'),
(34, 6, 1, 'Approved a purchase transaction at the register.', '2026-01-15 17:00:00'), (34, 6, 1, 'priya.patel', 'Priya Patel', 'STAFF', 'Downtown Branch', 'Approved a purchase transaction at the register.', '2026-01-15 17:00:00'),
(35, 7, 2, 'Updated a pet availability record.', '2026-01-16 02:00:00'), (35, 7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Updated a pet availability record.', '2026-01-16 02:00:00'),
(36, 8, 2, 'Completed a grooming appointment handoff.', '2026-01-16 11:00:00'), (36, 8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Completed a grooming appointment handoff.', '2026-01-16 11:00:00'),
(37, 9, 2, 'Checked a pending adoption record.', '2026-01-16 20:00:00'), (37, 9, 2, 'lucas.turner', 'Lucas Turner', 'STAFF', 'North Branch', 'Checked a pending adoption record.', '2026-01-16 20:00:00'),
(38, 10, 2, 'Reviewed a refund request tied to an original sale.', '2026-01-17 05:00:00'), (38, 10, 2, 'nina.green', 'Nina Green', 'STAFF', 'North Branch', 'Reviewed a refund request tied to an original sale.', '2026-01-17 05:00:00'),
(39, 11, 3, 'Answered a customer support conversation.', '2026-01-17 14:00:00'), (39, 11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Answered a customer support conversation.', '2026-01-17 14:00:00'),
(40, 12, 3, 'Updated a product detail for the catalogue.', '2026-01-17 23:00:00'), (40, 12, 3, 'daniel.moore', 'Daniel Moore', 'STAFF', 'West Side Store', 'Updated a product detail for the catalogue.', '2026-01-17 23:00:00'),
(41, 13, 3, 'Reviewed store inventory adjustments.', '2026-01-18 08:00:00'), (41, 13, 3, 'chloe.martin', 'Chloe Martin', 'STAFF', 'West Side Store', 'Reviewed store inventory adjustments.', '2026-01-18 08:00:00'),
(42, 14, 3, 'Approved a purchase transaction at the register.', '2026-01-18 17:00:00'), (42, 14, 3, 'owen.baker', 'Owen Baker', 'STAFF', 'West Side Store', 'Approved a purchase transaction at the register.', '2026-01-18 17:00:00'),
(43, 1, 1, 'Updated a pet availability record.', '2026-01-19 02:00:00'), (43, 1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated a pet availability record.', '2026-01-19 02:00:00'),
(44, 2, 2, 'Completed a grooming appointment handoff.', '2026-01-19 11:00:00'), (44, 2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Completed a grooming appointment handoff.', '2026-01-19 11:00:00'),
(45, 3, 1, 'Checked a pending adoption record.', '2026-01-19 20:00:00'), (45, 3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Checked a pending adoption record.', '2026-01-19 20:00:00'),
(46, 4, 1, 'Reviewed a refund request tied to an original sale.', '2026-01-20 05:00:00'), (46, 4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Reviewed a refund request tied to an original sale.', '2026-01-20 05:00:00'),
(47, 5, 1, 'Answered a customer support conversation.', '2026-01-20 14:00:00'), (47, 5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Answered a customer support conversation.', '2026-01-20 14:00:00'),
(48, 6, 1, 'Updated a product detail for the catalogue.', '2026-01-20 23:00:00'), (48, 6, 1, 'priya.patel', 'Priya Patel', 'STAFF', 'Downtown Branch', 'Updated a product detail for the catalogue.', '2026-01-20 23:00:00'),
(49, 7, 2, 'Reviewed store inventory adjustments.', '2026-01-21 08:00:00'), (49, 7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Reviewed store inventory adjustments.', '2026-01-21 08:00:00'),
(50, 8, 2, 'Approved a purchase transaction at the register.', '2026-01-21 17:00:00'), (50, 8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Approved a purchase transaction at the register.', '2026-01-21 17:00:00'),
(51, 9, 2, 'Updated a pet availability record.', '2026-01-22 02:00:00'), (51, 9, 2, 'lucas.turner', 'Lucas Turner', 'STAFF', 'North Branch', 'Updated a pet availability record.', '2026-01-22 02:00:00'),
(52, 10, 2, 'Completed a grooming appointment handoff.', '2026-01-22 11:00:00'), (52, 10, 2, 'nina.green', 'Nina Green', 'STAFF', 'North Branch', 'Completed a grooming appointment handoff.', '2026-01-22 11:00:00'),
(53, 11, 3, 'Checked a pending adoption record.', '2026-01-22 20:00:00'), (53, 11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Checked a pending adoption record.', '2026-01-22 20:00:00'),
(54, 12, 3, 'Reviewed a refund request tied to an original sale.', '2026-01-23 05:00:00'), (54, 12, 3, 'daniel.moore', 'Daniel Moore', 'STAFF', 'West Side Store', 'Reviewed a refund request tied to an original sale.', '2026-01-23 05:00:00'),
(55, 13, 3, 'Answered a customer support conversation.', '2026-01-23 14:00:00'), (55, 13, 3, 'chloe.martin', 'Chloe Martin', 'STAFF', 'West Side Store', 'Answered a customer support conversation.', '2026-01-23 14:00:00'),
(56, 14, 3, 'Updated a product detail for the catalogue.', '2026-01-23 23:00:00'), (56, 14, 3, 'owen.baker', 'Owen Baker', 'STAFF', 'West Side Store', 'Updated a product detail for the catalogue.', '2026-01-23 23:00:00'),
(57, 1, 1, 'Reviewed store inventory adjustments.', '2026-01-24 08:00:00'), (57, 1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Reviewed store inventory adjustments.', '2026-01-24 08:00:00'),
(58, 2, 2, 'Approved a purchase transaction at the register.', '2026-01-24 17:00:00'), (58, 2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Approved a purchase transaction at the register.', '2026-01-24 17:00:00'),
(59, 3, 1, 'Updated a pet availability record.', '2026-01-25 02:00:00'), (59, 3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Updated a pet availability record.', '2026-01-25 02:00:00'),
(60, 4, 1, 'Completed a grooming appointment handoff.', '2026-01-25 11:00:00'), (60, 4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Completed a grooming appointment handoff.', '2026-01-25 11:00:00'),
(61, 5, 1, 'Checked a pending adoption record.', '2026-01-25 20:00:00'), (61, 5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Checked a pending adoption record.', '2026-01-25 20:00:00'),
(62, 6, 1, 'Reviewed a refund request tied to an original sale.', '2026-01-26 05:00:00'), (62, 6, 1, 'priya.patel', 'Priya Patel', 'STAFF', 'Downtown Branch', 'Reviewed a refund request tied to an original sale.', '2026-01-26 05:00:00'),
(63, 7, 2, 'Answered a customer support conversation.', '2026-01-26 14:00:00'), (63, 7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Answered a customer support conversation.', '2026-01-26 14:00:00'),
(64, 8, 2, 'Updated a product detail for the catalogue.', '2026-01-26 23:00:00'), (64, 8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Updated a product detail for the catalogue.', '2026-01-26 23:00:00'),
(65, 9, 2, 'Reviewed store inventory adjustments.', '2026-01-27 08:00:00'), (65, 9, 2, 'lucas.turner', 'Lucas Turner', 'STAFF', 'North Branch', 'Reviewed store inventory adjustments.', '2026-01-27 08:00:00'),
(66, 10, 2, 'Approved a purchase transaction at the register.', '2026-01-27 17:00:00'), (66, 10, 2, 'nina.green', 'Nina Green', 'STAFF', 'North Branch', 'Approved a purchase transaction at the register.', '2026-01-27 17:00:00'),
(67, 11, 3, 'Updated a pet availability record.', '2026-01-28 02:00:00'), (67, 11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Updated a pet availability record.', '2026-01-28 02:00:00'),
(68, 12, 3, 'Completed a grooming appointment handoff.', '2026-01-28 11:00:00'), (68, 12, 3, 'daniel.moore', 'Daniel Moore', 'STAFF', 'West Side Store', 'Completed a grooming appointment handoff.', '2026-01-28 11:00:00'),
(69, 13, 3, 'Checked a pending adoption record.', '2026-01-28 20:00:00'), (69, 13, 3, 'chloe.martin', 'Chloe Martin', 'STAFF', 'West Side Store', 'Checked a pending adoption record.', '2026-01-28 20:00:00'),
(70, 14, 3, 'Reviewed a refund request tied to an original sale.', '2026-01-29 05:00:00'), (70, 14, 3, 'owen.baker', 'Owen Baker', 'STAFF', 'West Side Store', 'Reviewed a refund request tied to an original sale.', '2026-01-29 05:00:00'),
(71, 1, 1, 'Answered a customer support conversation.', '2026-01-29 14:00:00'), (71, 1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Answered a customer support conversation.', '2026-01-29 14:00:00'),
(72, 2, 2, 'Updated a product detail for the catalogue.', '2026-01-29 23:00:00'), (72, 2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Updated a product detail for the catalogue.', '2026-01-29 23:00:00'),
(73, 3, 1, 'Reviewed store inventory adjustments.', '2026-01-30 08:00:00'), (73, 3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Reviewed store inventory adjustments.', '2026-01-30 08:00:00'),
(74, 4, 1, 'Approved a purchase transaction at the register.', '2026-01-30 17:00:00'), (74, 4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Approved a purchase transaction at the register.', '2026-01-30 17:00:00'),
(75, 5, 1, 'Updated a pet availability record.', '2026-01-31 02:00:00'), (75, 5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Updated a pet availability record.', '2026-01-31 02:00:00'),
(76, 6, 1, 'Completed a grooming appointment handoff.', '2026-01-31 11:00:00'), (76, 6, 1, 'priya.patel', 'Priya Patel', 'STAFF', 'Downtown Branch', 'Completed a grooming appointment handoff.', '2026-01-31 11:00:00'),
(77, 7, 2, 'Checked a pending adoption record.', '2026-01-31 20:00:00'), (77, 7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Checked a pending adoption record.', '2026-01-31 20:00:00'),
(78, 8, 2, 'Reviewed a refund request tied to an original sale.', '2026-02-01 05:00:00'), (78, 8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Reviewed a refund request tied to an original sale.', '2026-02-01 05:00:00'),
(79, 9, 2, 'Answered a customer support conversation.', '2026-02-01 14:00:00'), (79, 9, 2, 'lucas.turner', 'Lucas Turner', 'STAFF', 'North Branch', 'Answered a customer support conversation.', '2026-02-01 14:00:00'),
(80, 10, 2, 'Updated a product detail for the catalogue.', '2026-02-01 23:00:00'), (80, 10, 2, 'nina.green', 'Nina Green', 'STAFF', 'North Branch', 'Updated a product detail for the catalogue.', '2026-02-01 23:00:00'),
(81, 11, 3, 'Reviewed store inventory adjustments.', '2026-02-02 08:00:00'), (81, 11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Reviewed store inventory adjustments.', '2026-02-02 08:00:00'),
(82, 12, 3, 'Approved a purchase transaction at the register.', '2026-02-02 17:00:00'), (82, 12, 3, 'daniel.moore', 'Daniel Moore', 'STAFF', 'West Side Store', 'Approved a purchase transaction at the register.', '2026-02-02 17:00:00'),
(83, 13, 3, 'Updated a pet availability record.', '2026-02-03 02:00:00'), (83, 13, 3, 'chloe.martin', 'Chloe Martin', 'STAFF', 'West Side Store', 'Updated a pet availability record.', '2026-02-03 02:00:00'),
(84, 14, 3, 'Completed a grooming appointment handoff.', '2026-02-03 11:00:00'), (84, 14, 3, 'owen.baker', 'Owen Baker', 'STAFF', 'West Side Store', 'Completed a grooming appointment handoff.', '2026-02-03 11:00:00'),
(85, 1, 1, 'Checked a pending adoption record.', '2026-02-03 20:00:00'), (85, 1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Checked a pending adoption record.', '2026-02-03 20:00:00'),
(86, 2, 2, 'Reviewed a refund request tied to an original sale.', '2026-02-04 05:00:00'), (86, 2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Reviewed a refund request tied to an original sale.', '2026-02-04 05:00:00'),
(87, 3, 1, 'Answered a customer support conversation.', '2026-02-04 14:00:00'), (87, 3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Answered a customer support conversation.', '2026-02-04 14:00:00'),
(88, 4, 1, 'Updated a product detail for the catalogue.', '2026-02-04 23:00:00'), (88, 4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Updated a product detail for the catalogue.', '2026-02-04 23:00:00'),
(89, 5, 1, 'Reviewed store inventory adjustments.', '2026-02-05 08:00:00'), (89, 5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Reviewed store inventory adjustments.', '2026-02-05 08:00:00'),
(90, 6, 1, 'Approved a purchase transaction at the register.', '2026-02-05 17:00:00'), (90, 6, 1, 'priya.patel', 'Priya Patel', 'STAFF', 'Downtown Branch', 'Approved a purchase transaction at the register.', '2026-02-05 17:00:00'),
(91, 7, 2, 'Updated a pet availability record.', '2026-02-06 02:00:00'), (91, 7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Updated a pet availability record.', '2026-02-06 02:00:00'),
(92, 8, 2, 'Completed a grooming appointment handoff.', '2026-02-06 11:00:00'), (92, 8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Completed a grooming appointment handoff.', '2026-02-06 11:00:00'),
(93, 9, 2, 'Checked a pending adoption record.', '2026-02-06 20:00:00'), (93, 9, 2, 'lucas.turner', 'Lucas Turner', 'STAFF', 'North Branch', 'Checked a pending adoption record.', '2026-02-06 20:00:00'),
(94, 10, 2, 'Reviewed a refund request tied to an original sale.', '2026-02-07 05:00:00'), (94, 10, 2, 'nina.green', 'Nina Green', 'STAFF', 'North Branch', 'Reviewed a refund request tied to an original sale.', '2026-02-07 05:00:00'),
(95, 11, 3, 'Answered a customer support conversation.', '2026-02-07 14:00:00'), (95, 11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Answered a customer support conversation.', '2026-02-07 14:00:00'),
(96, 12, 3, 'Updated a product detail for the catalogue.', '2026-02-07 23:00:00'), (96, 12, 3, 'daniel.moore', 'Daniel Moore', 'STAFF', 'West Side Store', 'Updated a product detail for the catalogue.', '2026-02-07 23:00:00'),
(97, 13, 3, 'Reviewed store inventory adjustments.', '2026-02-08 08:00:00'), (97, 13, 3, 'chloe.martin', 'Chloe Martin', 'STAFF', 'West Side Store', 'Reviewed store inventory adjustments.', '2026-02-08 08:00:00'),
(98, 14, 3, 'Approved a purchase transaction at the register.', '2026-02-08 17:00:00'), (98, 14, 3, 'owen.baker', 'Owen Baker', 'STAFF', 'West Side Store', 'Approved a purchase transaction at the register.', '2026-02-08 17:00:00'),
(99, 1, 1, 'Updated a pet availability record.', '2026-02-09 02:00:00'), (99, 1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Updated a pet availability record.', '2026-02-09 02:00:00'),
(100, 2, 2, 'Completed a grooming appointment handoff.', '2026-02-09 11:00:00'), (100, 2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Completed a grooming appointment handoff.', '2026-02-09 11:00:00'),
(101, 3, 1, 'Checked a pending adoption record.', '2026-02-09 20:00:00'), (101, 3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Checked a pending adoption record.', '2026-02-09 20:00:00'),
(102, 4, 1, 'Reviewed a refund request tied to an original sale.', '2026-02-10 05:00:00'), (102, 4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Reviewed a refund request tied to an original sale.', '2026-02-10 05:00:00'),
(103, 5, 1, 'Answered a customer support conversation.', '2026-02-10 14:00:00'), (103, 5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Answered a customer support conversation.', '2026-02-10 14:00:00'),
(104, 6, 1, 'Updated a product detail for the catalogue.', '2026-02-10 23:00:00'), (104, 6, 1, 'priya.patel', 'Priya Patel', 'STAFF', 'Downtown Branch', 'Updated a product detail for the catalogue.', '2026-02-10 23:00:00'),
(105, 7, 2, 'Reviewed store inventory adjustments.', '2026-02-11 08:00:00'), (105, 7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Reviewed store inventory adjustments.', '2026-02-11 08:00:00'),
(106, 8, 2, 'Approved a purchase transaction at the register.', '2026-02-11 17:00:00'), (106, 8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Approved a purchase transaction at the register.', '2026-02-11 17:00:00'),
(107, 9, 2, 'Updated a pet availability record.', '2026-02-12 02:00:00'), (107, 9, 2, 'lucas.turner', 'Lucas Turner', 'STAFF', 'North Branch', 'Updated a pet availability record.', '2026-02-12 02:00:00'),
(108, 10, 2, 'Completed a grooming appointment handoff.', '2026-02-12 11:00:00'), (108, 10, 2, 'nina.green', 'Nina Green', 'STAFF', 'North Branch', 'Completed a grooming appointment handoff.', '2026-02-12 11:00:00'),
(109, 11, 3, 'Checked a pending adoption record.', '2026-02-12 20:00:00'), (109, 11, 3, 'lisa.williams', 'Lisa Williams', 'STAFF', 'West Side Store', 'Checked a pending adoption record.', '2026-02-12 20:00:00'),
(110, 12, 3, 'Reviewed a refund request tied to an original sale.', '2026-02-13 05:00:00'), (110, 12, 3, 'daniel.moore', 'Daniel Moore', 'STAFF', 'West Side Store', 'Reviewed a refund request tied to an original sale.', '2026-02-13 05:00:00'),
(111, 13, 3, 'Answered a customer support conversation.', '2026-02-13 14:00:00'), (111, 13, 3, 'chloe.martin', 'Chloe Martin', 'STAFF', 'West Side Store', 'Answered a customer support conversation.', '2026-02-13 14:00:00'),
(112, 14, 3, 'Updated a product detail for the catalogue.', '2026-02-13 23:00:00'), (112, 14, 3, 'owen.baker', 'Owen Baker', 'STAFF', 'West Side Store', 'Updated a product detail for the catalogue.', '2026-02-13 23:00:00'),
(113, 1, 1, 'Reviewed store inventory adjustments.', '2026-02-14 08:00:00'), (113, 1, 1, 'admin', 'Admin User', 'ADMIN', 'Downtown Branch', 'Reviewed store inventory adjustments.', '2026-02-14 08:00:00'),
(114, 2, 2, 'Approved a purchase transaction at the register.', '2026-02-14 17:00:00'), (114, 2, 2, 'morgan.lee', 'Morgan Lee', 'ADMIN', 'North Branch', 'Approved a purchase transaction at the register.', '2026-02-14 17:00:00'),
(115, 3, 1, 'Updated a pet availability record.', '2026-02-15 02:00:00'), (115, 3, 1, 'staff', 'Staff User', 'STAFF', 'Downtown Branch', 'Updated a pet availability record.', '2026-02-15 02:00:00'),
(116, 4, 1, 'Completed a grooming appointment handoff.', '2026-02-15 11:00:00'), (116, 4, 1, 'sara.smith', 'Sara Smith', 'STAFF', 'Downtown Branch', 'Completed a grooming appointment handoff.', '2026-02-15 11:00:00'),
(117, 5, 1, 'Checked a pending adoption record.', '2026-02-15 20:00:00'), (117, 5, 1, 'david.brown', 'David Brown', 'STAFF', 'Downtown Branch', 'Checked a pending adoption record.', '2026-02-15 20:00:00'),
(118, 6, 1, 'Reviewed a refund request tied to an original sale.', '2026-02-16 05:00:00'), (118, 6, 1, 'priya.patel', 'Priya Patel', 'STAFF', 'Downtown Branch', 'Reviewed a refund request tied to an original sale.', '2026-02-16 05:00:00'),
(119, 7, 2, 'Answered a customer support conversation.', '2026-02-16 14:00:00'), (119, 7, 2, 'michael.johnson', 'Michael Johnson', 'STAFF', 'North Branch', 'Answered a customer support conversation.', '2026-02-16 14:00:00'),
(120, 8, 2, 'Updated a product detail for the catalogue.', '2026-02-16 23:00:00'); (120, 8, 2, 'emma.davis', 'Emma Davis', 'STAFF', 'North Branch', 'Updated a product detail for the catalogue.', '2026-02-16 23:00:00');

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Some files were not shown because too many files have changed in this diff Show More