Add customer registration, profile management and refunds

This commit is contained in:
2026-03-08 10:50:03 -06:00
parent 6b182f2cc2
commit 2dedd5508f
21 changed files with 1268 additions and 14 deletions

4
.gitignore vendored
View File

@@ -34,3 +34,7 @@ build/
### Mac ### ### Mac ###
.DS_Store .DS_Store
### Project Specific ###
tmp/
uploads/

View File

@@ -34,5 +34,15 @@ public class DataInitializer implements CommandLineRunner {
staff.setRole(User.Role.STAFF); staff.setRole(User.Role.STAFF);
userRepository.save(staff); userRepository.save(staff);
} }
if (userRepository.findByUsername("customer").isEmpty()) {
User customer = new User();
customer.setUsername("customer");
customer.setPassword(passwordEncoder.encode("customer123"));
customer.setEmail("customer@petshop.com");
customer.setFullName("Test Customer");
customer.setRole(User.Role.CUSTOMER);
userRepository.save(customer);
}
} }
} }

View File

@@ -1,7 +1,11 @@
package com.petshop.backend.controller; package com.petshop.backend.controller;
import com.petshop.backend.dto.auth.AvatarUploadResponse;
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.RegisterRequest;
import com.petshop.backend.dto.auth.RegisterResponse;
import com.petshop.backend.dto.auth.UserInfoResponse; import com.petshop.backend.dto.auth.UserInfoResponse;
import com.petshop.backend.entity.User; import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository; import com.petshop.backend.repository.UserRepository;
@@ -18,9 +22,17 @@ import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID;
@RestController @RestController
@RequestMapping("/api/v1/auth") @RequestMapping("/api/v1/auth")
@@ -38,6 +50,46 @@ public class AuthController {
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
} }
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Username already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Email already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setEmail(request.getEmail());
user.setFullName(request.getFullName());
user.setRole(User.Role.CUSTOMER);
User savedUser = userRepository.save(user);
UserDetails userDetails = new org.springframework.security.core.userdetails.User(
savedUser.getUsername(),
savedUser.getPassword(),
java.util.Collections.emptyList()
);
String token = jwtUtil.generateToken(userDetails);
return ResponseEntity.status(HttpStatus.CREATED).body(new RegisterResponse(
savedUser.getId(),
savedUser.getUsername(),
savedUser.getEmail(),
savedUser.getRole().name(),
token
));
}
@PostMapping("/login") @PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) { public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
try { try {
@@ -80,10 +132,157 @@ public class AuthController {
return ResponseEntity.ok(new UserInfoResponse( return ResponseEntity.ok(new UserInfoResponse(
user.getId(), user.getId(),
user.getUsername(), user.getUsername(),
user.getEmail(),
user.getFullName(),
user.getAvatarUrl(),
user.getRole().name() user.getRole().name()
)); ));
} }
@PutMapping("/me")
public ResponseEntity<?> updateProfile(@Valid @RequestBody ProfileUpdateRequest request) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
if (request.getUsername() != null && !request.getUsername().equals(user.getUsername())) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Username already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
user.setUsername(request.getUsername());
}
if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) {
if (userRepository.findByEmail(request.getEmail()).isPresent()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Email already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
user.setEmail(request.getEmail());
}
if (request.getFullName() != null) {
user.setFullName(request.getFullName());
}
if (request.getPassword() != null && !request.getPassword().isEmpty()) {
user.setPassword(passwordEncoder.encode(request.getPassword()));
}
User updatedUser = userRepository.save(user);
return ResponseEntity.ok(new UserInfoResponse(
updatedUser.getId(),
updatedUser.getUsername(),
updatedUser.getEmail(),
updatedUser.getFullName(),
updatedUser.getAvatarUrl(),
updatedUser.getRole().name()
));
}
@PostMapping("/me/avatar")
public ResponseEntity<?> uploadAvatar(@RequestParam("avatar") MultipartFile file) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
if (file.isEmpty()) {
Map<String, String> error = new HashMap<>();
error.put("message", "Please select a file to upload");
return ResponseEntity.badRequest().body(error);
}
if (file.getSize() > 5 * 1024 * 1024) {
Map<String, String> error = new HashMap<>();
error.put("message", "File size must not exceed 5MB");
return ResponseEntity.badRequest().body(error);
}
String contentType = file.getContentType();
if (contentType == null || (!contentType.equals("image/jpeg") && !contentType.equals("image/png") && !contentType.equals("image/gif"))) {
Map<String, String> error = new HashMap<>();
error.put("message", "Only JPG, PNG, and GIF images are allowed");
return ResponseEntity.badRequest().body(error);
}
try {
String uploadDir = "uploads/avatars";
File directory = new File(uploadDir);
if (!directory.exists()) {
directory.mkdirs();
}
String originalFilename = file.getOriginalFilename();
String extension = originalFilename != null && originalFilename.contains(".")
? originalFilename.substring(originalFilename.lastIndexOf("."))
: ".jpg";
String filename = UUID.randomUUID().toString() + extension;
Path filePath = Paths.get(uploadDir, filename);
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
String avatarUrl = "/uploads/avatars/" + filename;
user.setAvatarUrl(avatarUrl);
userRepository.save(user);
return ResponseEntity.ok(new AvatarUploadResponse(avatarUrl, "Avatar uploaded successfully"));
} catch (IOException e) {
Map<String, String> error = new HashMap<>();
error.put("message", "Failed to upload avatar: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
@GetMapping("/me/avatar")
public ResponseEntity<?> getAvatar() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
if (user.getAvatarUrl() == null || user.getAvatarUrl().isEmpty()) {
Map<String, String> error = new HashMap<>();
error.put("message", "No avatar uploaded");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
Map<String, String> response = new HashMap<>();
response.put("avatarUrl", user.getAvatarUrl());
return ResponseEntity.ok(response);
}
@DeleteMapping("/me/avatar")
public ResponseEntity<?> deleteAvatar() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
if (user.getAvatarUrl() != null && !user.getAvatarUrl().isEmpty()) {
try {
Path filePath = Paths.get("." + user.getAvatarUrl());
Files.deleteIfExists(filePath);
} catch (IOException e) {
}
user.setAvatarUrl(null);
userRepository.save(user);
}
Map<String, String> response = new HashMap<>();
response.put("message", "Avatar deleted successfully");
return ResponseEntity.ok(response);
}
@PostMapping("/logout") @PostMapping("/logout")
public ResponseEntity<?> logout() { public ResponseEntity<?> logout() {
Map<String, String> response = new HashMap<>(); Map<String, String> response = new HashMap<>();

View File

@@ -0,0 +1,113 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.refund.RefundRequest;
import com.petshop.backend.dto.refund.RefundResponse;
import com.petshop.backend.dto.refund.RefundUpdateRequest;
import com.petshop.backend.service.RefundService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/refunds")
public class RefundController {
private final RefundService refundService;
public RefundController(RefundService refundService) {
this.refundService = refundService;
}
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<?> createRefund(@Valid @RequestBody RefundRequest request) {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null;
RefundResponse refund = refundService.createRefund(request, customerId);
return ResponseEntity.status(HttpStatus.CREATED).body(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.badRequest().body(error);
}
}
@GetMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<List<RefundResponse>> getAllRefunds() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null;
List<RefundResponse> refunds = refundService.getAllRefunds(customerId);
return ResponseEntity.ok(refunds);
}
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<?> getRefundById(@PathVariable Long id) {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = role != null && role.equals("CUSTOMER") ? 1L : null;
RefundResponse refund = refundService.getRefundById(id, customerId);
return ResponseEntity.ok(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<?> updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) {
try {
RefundResponse refund = refundService.updateRefundStatus(id, request.getStatus());
return ResponseEntity.ok(refund);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.badRequest().body(error);
}
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> deleteRefund(@PathVariable Long id) {
try {
refundService.deleteRefund(id);
Map<String, String> response = new HashMap<>();
response.put("message", "Refund deleted successfully");
return ResponseEntity.ok(response);
} catch (RuntimeException e) {
Map<String, String> error = new HashMap<>();
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
}

View File

@@ -0,0 +1,54 @@
package com.petshop.backend.dto.auth;
import java.util.Objects;
public class AvatarUploadResponse {
private String avatarUrl;
private String message;
public AvatarUploadResponse() {
}
public AvatarUploadResponse(String avatarUrl, String message) {
this.avatarUrl = avatarUrl;
this.message = message;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AvatarUploadResponse that = (AvatarUploadResponse) o;
return Objects.equals(avatarUrl, that.avatarUrl) &&
Objects.equals(message, that.message);
}
@Override
public int hashCode() {
return Objects.hash(avatarUrl, message);
}
@Override
public String toString() {
return "AvatarUploadResponse{" +
"avatarUrl='" + avatarUrl + '\'' +
", message='" + message + '\'' +
'}';
}
}

View File

@@ -0,0 +1,77 @@
package com.petshop.backend.dto.auth;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
import java.util.Objects;
public class ProfileUpdateRequest {
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@Email(message = "Email must be valid")
private String email;
@Size(max = 100, message = "Full name must not exceed 100 characters")
private String fullName;
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProfileUpdateRequest that = (ProfileUpdateRequest) o;
return Objects.equals(username, that.username) &&
Objects.equals(email, that.email) &&
Objects.equals(fullName, that.fullName) &&
Objects.equals(password, that.password);
}
@Override
public int hashCode() {
return Objects.hash(username, email, fullName, password);
}
@Override
public String toString() {
return "ProfileUpdateRequest{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", password='" + password + '\'' +
'}';
}
}

View File

@@ -0,0 +1,82 @@
package com.petshop.backend.dto.auth;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.util.Objects;
public class RegisterRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@NotBlank(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@NotBlank(message = "Full name is required")
@Size(max = 100, message = "Full name must not exceed 100 characters")
private String fullName;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RegisterRequest that = (RegisterRequest) o;
return Objects.equals(username, that.username) &&
Objects.equals(password, that.password) &&
Objects.equals(email, that.email) &&
Objects.equals(fullName, that.fullName);
}
@Override
public int hashCode() {
return Objects.hash(username, password, email, fullName);
}
@Override
public String toString() {
return "RegisterRequest{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
'}';
}
}

View File

@@ -0,0 +1,90 @@
package com.petshop.backend.dto.auth;
import java.util.Objects;
public class RegisterResponse {
private Long id;
private String username;
private String email;
private String role;
private String token;
public RegisterResponse() {
}
public RegisterResponse(Long id, String username, String email, String role, String token) {
this.id = id;
this.username = username;
this.email = email;
this.role = role;
this.token = token;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RegisterResponse that = (RegisterResponse) o;
return Objects.equals(id, that.id) &&
Objects.equals(username, that.username) &&
Objects.equals(email, that.email) &&
Objects.equals(role, that.role) &&
Objects.equals(token, that.token);
}
@Override
public int hashCode() {
return Objects.hash(id, username, email, role, token);
}
@Override
public String toString() {
return "RegisterResponse{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
", role='" + role + '\'' +
", token='" + token + '\'' +
'}';
}
}

View File

@@ -5,14 +5,20 @@ import java.util.Objects;
public class UserInfoResponse { public class UserInfoResponse {
private Long id; private Long id;
private String username; private String username;
private String email;
private String fullName;
private String avatarUrl;
private String role; private String role;
public UserInfoResponse() { public UserInfoResponse() {
} }
public UserInfoResponse(Long id, String username, String role) { public UserInfoResponse(Long id, String username, String email, String fullName, String avatarUrl, String role) {
this.id = id; this.id = id;
this.username = username; this.username = username;
this.email = email;
this.fullName = fullName;
this.avatarUrl = avatarUrl;
this.role = role; this.role = role;
} }
@@ -32,6 +38,30 @@ public class UserInfoResponse {
this.username = username; this.username = username;
} }
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public String getRole() { public String getRole() {
return role; return role;
} }
@@ -45,12 +75,17 @@ public class UserInfoResponse {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
UserInfoResponse that = (UserInfoResponse) o; UserInfoResponse that = (UserInfoResponse) o;
return Objects.equals(id, that.id) && Objects.equals(username, that.username) && Objects.equals(role, that.role); return Objects.equals(id, that.id) &&
Objects.equals(username, that.username) &&
Objects.equals(email, that.email) &&
Objects.equals(fullName, that.fullName) &&
Objects.equals(avatarUrl, that.avatarUrl) &&
Objects.equals(role, that.role);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(id, username, role); return Objects.hash(id, username, email, fullName, avatarUrl, role);
} }
@Override @Override
@@ -58,6 +93,9 @@ public class UserInfoResponse {
return "UserInfoResponse{" + return "UserInfoResponse{" +
"id=" + id + "id=" + id +
", username='" + username + '\'' + ", username='" + username + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", avatarUrl='" + avatarUrl + '\'' +
", role='" + role + '\'' + ", role='" + role + '\'' +
'}'; '}';
} }

View File

@@ -0,0 +1,51 @@
package com.petshop.backend.dto.refund;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.Objects;
public class RefundRequest {
@NotNull(message = "Sale ID is required")
private Long saleId;
@NotBlank(message = "Reason is required")
private String reason;
public Long getSaleId() {
return saleId;
}
public void setSaleId(Long saleId) {
this.saleId = saleId;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RefundRequest that = (RefundRequest) o;
return Objects.equals(saleId, that.saleId) &&
Objects.equals(reason, that.reason);
}
@Override
public int hashCode() {
return Objects.hash(saleId, reason);
}
@Override
public String toString() {
return "RefundRequest{" +
"saleId=" + saleId +
", reason='" + reason + '\'' +
'}';
}
}

View File

@@ -0,0 +1,128 @@
package com.petshop.backend.dto.refund;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Objects;
public class RefundResponse {
private Long id;
private Long saleId;
private Long customerId;
private BigDecimal amount;
private String reason;
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public RefundResponse() {
}
public RefundResponse(Long id, Long saleId, Long customerId, BigDecimal amount, String reason, String status, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.saleId = saleId;
this.customerId = customerId;
this.amount = amount;
this.reason = reason;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getSaleId() {
return saleId;
}
public void setSaleId(Long saleId) {
this.saleId = saleId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RefundResponse that = (RefundResponse) o;
return Objects.equals(id, that.id) &&
Objects.equals(saleId, that.saleId) &&
Objects.equals(customerId, that.customerId) &&
Objects.equals(amount, that.amount) &&
Objects.equals(reason, that.reason) &&
Objects.equals(status, that.status) &&
Objects.equals(createdAt, that.createdAt) &&
Objects.equals(updatedAt, that.updatedAt);
}
@Override
public int hashCode() {
return Objects.hash(id, saleId, customerId, amount, reason, status, createdAt, updatedAt);
}
@Override
public String toString() {
return "RefundResponse{" +
"id=" + id +
", saleId=" + saleId +
", customerId=" + customerId +
", amount=" + amount +
", reason='" + reason + '\'' +
", status='" + status + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';
}
}

View File

@@ -0,0 +1,37 @@
package com.petshop.backend.dto.refund;
import jakarta.validation.constraints.NotBlank;
import java.util.Objects;
public class RefundUpdateRequest {
@NotBlank(message = "Status is required")
private String status;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RefundUpdateRequest that = (RefundUpdateRequest) o;
return Objects.equals(status, that.status);
}
@Override
public int hashCode() {
return Objects.hash(status);
}
@Override
public String toString() {
return "RefundUpdateRequest{" +
"status='" + status + '\'' +
'}';
}
}

View File

@@ -0,0 +1,151 @@
package com.petshop.backend.entity;
import jakarta.persistence.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Objects;
@Entity
@Table(name = "refund")
public class Refund {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private Long saleId;
@Column(nullable = false)
private Long customerId;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal amount;
@Column(nullable = false, length = 500)
private String reason;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private RefundStatus status;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
public enum RefundStatus {
PENDING, APPROVED, REJECTED
}
public Refund() {
}
public Refund(Long id, Long saleId, Long customerId, BigDecimal amount, String reason, RefundStatus status, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.saleId = saleId;
this.customerId = customerId;
this.amount = amount;
this.reason = reason;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getSaleId() {
return saleId;
}
public void setSaleId(Long saleId) {
this.saleId = saleId;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
public RefundStatus getStatus() {
return status;
}
public void setStatus(RefundStatus status) {
this.status = status;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Refund refund = (Refund) o;
return Objects.equals(id, refund.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "Refund{" +
"id=" + id +
", saleId=" + saleId +
", customerId=" + customerId +
", amount=" + amount +
", reason='" + reason + '\'' +
", status=" + status +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
'}';
}
}

View File

@@ -29,6 +29,10 @@ public class Sale {
@JoinColumn(name = "storeId", nullable = false) @JoinColumn(name = "storeId", nullable = false)
private StoreLocation store; private StoreLocation store;
@ManyToOne
@JoinColumn(name = "customerId")
private Customer customer;
@Column(nullable = false, precision = 10, scale = 2) @Column(nullable = false, precision = 10, scale = 2)
private BigDecimal totalAmount; private BigDecimal totalAmount;
@@ -56,11 +60,12 @@ public class Sale {
public Sale() { public Sale() {
} }
public Sale(Long saleId, LocalDateTime saleDate, Employee employee, StoreLocation store, BigDecimal totalAmount, String paymentMethod, Boolean isRefund, Sale originalSale, List<SaleItem> items, LocalDateTime createdAt, LocalDateTime updatedAt) { public Sale(Long saleId, LocalDateTime saleDate, Employee employee, StoreLocation store, Customer customer, BigDecimal totalAmount, String paymentMethod, Boolean isRefund, Sale originalSale, List<SaleItem> items, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.saleId = saleId; this.saleId = saleId;
this.saleDate = saleDate; this.saleDate = saleDate;
this.employee = employee; this.employee = employee;
this.store = store; this.store = store;
this.customer = customer;
this.totalAmount = totalAmount; this.totalAmount = totalAmount;
this.paymentMethod = paymentMethod; this.paymentMethod = paymentMethod;
this.isRefund = isRefund; this.isRefund = isRefund;
@@ -102,6 +107,14 @@ public class Sale {
this.store = store; this.store = store;
} }
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public BigDecimal getTotalAmount() { public BigDecimal getTotalAmount() {
return totalAmount; return totalAmount;
} }
@@ -178,6 +191,7 @@ public class Sale {
", saleDate=" + saleDate + ", saleDate=" + saleDate +
", employee=" + employee + ", employee=" + employee +
", store=" + store + ", store=" + store +
", customer=" + customer +
", totalAmount=" + totalAmount + ", totalAmount=" + totalAmount +
", paymentMethod='" + paymentMethod + '\'' + ", paymentMethod='" + paymentMethod + '\'' +
", isRefund=" + isRefund + ", isRefund=" + isRefund +

View File

@@ -21,6 +21,15 @@ public class User {
@Column(nullable = false) @Column(nullable = false)
private String password; private String password;
@Column(unique = true, length = 100)
private String email;
@Column(length = 100)
private String fullName;
@Column(length = 255)
private String avatarUrl;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)") @Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)")
private Role role; private Role role;
@@ -34,16 +43,19 @@ public class User {
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
public enum Role { public enum Role {
STAFF, ADMIN CUSTOMER, STAFF, ADMIN
} }
public User() { public User() {
} }
public User(Long id, String username, String password, Role role, LocalDateTime createdAt, LocalDateTime updatedAt) { public User(Long id, String username, String password, String email, String fullName, String avatarUrl, Role role, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id; this.id = id;
this.username = username; this.username = username;
this.password = password; this.password = password;
this.email = email;
this.fullName = fullName;
this.avatarUrl = avatarUrl;
this.role = role; this.role = role;
this.createdAt = createdAt; this.createdAt = createdAt;
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
@@ -73,6 +85,30 @@ public class User {
this.password = password; this.password = password;
} }
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public Role getRole() { public Role getRole() {
return role; return role;
} }
@@ -116,6 +152,9 @@ public class User {
"id=" + id + "id=" + id +
", username='" + username + '\'' + ", username='" + username + '\'' +
", password='" + password + '\'' + ", password='" + password + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", avatarUrl='" + avatarUrl + '\'' +
", role=" + role + ", role=" + role +
", createdAt=" + createdAt + ", createdAt=" + createdAt +
", updatedAt=" + updatedAt + ", updatedAt=" + updatedAt +

View File

@@ -0,0 +1,13 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.Refund;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface RefundRepository extends JpaRepository<Refund, Long> {
List<Refund> findByCustomerId(Long customerId);
List<Refund> findBySaleId(Long saleId);
}

View File

@@ -13,6 +13,7 @@ import java.util.Optional;
@Repository @Repository
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username); Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
boolean existsByUsername(String username); boolean existsByUsername(String username);
@Query("SELECT u FROM User u WHERE " + @Query("SELECT u FROM User u WHERE " +

View File

@@ -36,18 +36,14 @@ public class SecurityConfig {
http http
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/login").permitAll() .requestMatchers("/api/v1/auth/login", "/api/v1/auth/register").permitAll()
.requestMatchers("/api/v1/health").permitAll() .requestMatchers("/api/v1/health").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/pets/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/pets/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/sales/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/sales/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/suppliers").hasRole("ADMIN") .requestMatchers(HttpMethod.GET, "/api/v1/services/**").permitAll()
.requestMatchers("/api/v1/inventory/**").hasRole("ADMIN") .requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll()
.requestMatchers("/api/v1/suppliers/**").hasRole("ADMIN")
.requestMatchers("/api/v1/product-suppliers/**").hasRole("ADMIN")
.requestMatchers("/api/v1/purchase-orders/**").hasRole("ADMIN")
.requestMatchers("/api/v1/users/**").hasRole("ADMIN")
.requestMatchers("/api/v1/analytics/**").hasRole("ADMIN")
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

View File

@@ -0,0 +1,111 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.refund.RefundRequest;
import com.petshop.backend.dto.refund.RefundResponse;
import com.petshop.backend.entity.Refund;
import com.petshop.backend.entity.Sale;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.RefundRepository;
import com.petshop.backend.repository.SaleRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class RefundService {
private final RefundRepository refundRepository;
private final SaleRepository saleRepository;
public RefundService(RefundRepository refundRepository, SaleRepository saleRepository) {
this.refundRepository = refundRepository;
this.saleRepository = saleRepository;
}
@Transactional
public RefundResponse createRefund(RefundRequest request, Long customerId) {
Sale sale = saleRepository.findById(request.getSaleId())
.orElseThrow(() -> new RuntimeException("Sale not found"));
if (sale.getCustomer() == null) {
throw new RuntimeException("Sale has no associated customer");
}
if (customerId != null && !sale.getCustomer().getCustomerId().equals(customerId)) {
throw new RuntimeException("You can only create refunds for your own purchases");
}
Refund refund = new Refund();
refund.setSaleId(sale.getSaleId());
refund.setCustomerId(sale.getCustomer().getCustomerId());
refund.setAmount(sale.getTotalAmount());
refund.setReason(request.getReason());
refund.setStatus(Refund.RefundStatus.PENDING);
Refund savedRefund = refundRepository.save(refund);
return toResponse(savedRefund);
}
public RefundResponse getRefundById(Long id, Long customerId) {
Refund refund = refundRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Refund not found"));
if (customerId != null && !refund.getCustomerId().equals(customerId)) {
throw new RuntimeException("You can only view your own refunds");
}
return toResponse(refund);
}
public List<RefundResponse> getAllRefunds(Long customerId) {
List<Refund> refunds;
if (customerId != null) {
refunds = refundRepository.findByCustomerId(customerId);
} else {
refunds = refundRepository.findAll();
}
return refunds.stream()
.map(this::toResponse)
.collect(Collectors.toList());
}
@Transactional
public RefundResponse updateRefundStatus(Long id, String status) {
Refund refund = refundRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Refund not found"));
try {
refund.setStatus(Refund.RefundStatus.valueOf(status.toUpperCase()));
} catch (IllegalArgumentException e) {
throw new RuntimeException("Invalid status: " + status);
}
Refund updatedRefund = refundRepository.save(refund);
return toResponse(updatedRefund);
}
@Transactional
public void deleteRefund(Long id) {
if (!refundRepository.existsById(id)) {
throw new RuntimeException("Refund not found");
}
refundRepository.deleteById(id);
}
private RefundResponse toResponse(Refund refund) {
return new RefundResponse(
refund.getId(),
refund.getSaleId(),
refund.getCustomerId(),
refund.getAmount(),
refund.getReason(),
refund.getStatus().name(),
refund.getCreatedAt(),
refund.getUpdatedAt()
);
}
}

View File

@@ -2,6 +2,12 @@ spring:
application: application:
name: petshop-backend name: petshop-backend
servlet:
multipart:
enabled: true
max-file-size: 5MB
max-request-size: 5MB
datasource: datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC} url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC}
username: ${SPRING_DATASOURCE_USERNAME:petshop} username: ${SPRING_DATASOURCE_USERNAME:petshop}

View File

@@ -152,12 +152,14 @@ CREATE TABLE IF NOT EXISTS sale (
paymentMethod VARCHAR(50) NOT NULL, paymentMethod VARCHAR(50) NOT NULL,
employeeId BIGINT NOT NULL, employeeId BIGINT NOT NULL,
storeId BIGINT NOT NULL, storeId BIGINT NOT NULL,
customerId BIGINT NULL,
isRefund BOOLEAN DEFAULT FALSE NOT NULL, isRefund BOOLEAN DEFAULT FALSE NOT NULL,
originalSaleId BIGINT NULL, originalSaleId BIGINT NULL,
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,
FOREIGN KEY (employeeId) REFERENCES employee(employeeId), FOREIGN KEY (employeeId) REFERENCES employee(employeeId),
FOREIGN KEY (storeId) REFERENCES storeLocation(storeId), FOREIGN KEY (storeId) REFERENCES storeLocation(storeId),
FOREIGN KEY (customerId) REFERENCES customer(customerId),
FOREIGN KEY (originalSaleId) REFERENCES sale(saleId) FOREIGN KEY (originalSaleId) REFERENCES sale(saleId)
); );
@@ -195,7 +197,45 @@ CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL, username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
email VARCHAR(100) UNIQUE,
fullName VARCHAR(100),
avatarUrl VARCHAR(255),
role VARCHAR(20) NOT NULL, role VARCHAR(20) NOT NULL,
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
); );
CREATE TABLE IF NOT EXISTS refund (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
saleId BIGINT NOT NULL,
customerId BIGINT NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
reason VARCHAR(500) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (saleId) REFERENCES sale(saleId),
FOREIGN KEY (customerId) REFERENCES customer(customerId)
);
CREATE TABLE IF NOT EXISTS conversation (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
customerId BIGINT NOT NULL,
staffId BIGINT,
status VARCHAR(20) DEFAULT 'OPEN',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (customerId) REFERENCES customer(customerId),
FOREIGN KEY (staffId) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS message (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
conversationId BIGINT NOT NULL,
senderId BIGINT NOT NULL,
content TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
isRead BOOLEAN DEFAULT FALSE,
FOREIGN KEY (conversationId) REFERENCES conversation(id),
FOREIGN KEY (senderId) REFERENCES users(id)
);