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 ###
.DS_Store
### Project Specific ###
tmp/
uploads/

View File

@@ -34,5 +34,15 @@ public class DataInitializer implements CommandLineRunner {
staff.setRole(User.Role.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;
import com.petshop.backend.dto.auth.AvatarUploadResponse;
import com.petshop.backend.dto.auth.LoginRequest;
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.entity.User;
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.crypto.password.PasswordEncoder;
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.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/v1/auth")
@@ -38,6 +50,46 @@ public class AuthController {
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")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
try {
@@ -80,10 +132,157 @@ public class AuthController {
return ResponseEntity.ok(new UserInfoResponse(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getFullName(),
user.getAvatarUrl(),
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")
public ResponseEntity<?> logout() {
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 {
private Long id;
private String username;
private String email;
private String fullName;
private String avatarUrl;
private String role;
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.username = username;
this.email = email;
this.fullName = fullName;
this.avatarUrl = avatarUrl;
this.role = role;
}
@@ -32,6 +38,30 @@ public class UserInfoResponse {
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() {
return role;
}
@@ -45,12 +75,17 @@ public class UserInfoResponse {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
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
public int hashCode() {
return Objects.hash(id, username, role);
return Objects.hash(id, username, email, fullName, avatarUrl, role);
}
@Override
@@ -58,6 +93,9 @@ public class UserInfoResponse {
return "UserInfoResponse{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", avatarUrl='" + avatarUrl + '\'' +
", 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)
private StoreLocation store;
@ManyToOne
@JoinColumn(name = "customerId")
private Customer customer;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal totalAmount;
@@ -56,11 +60,12 @@ public class 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.saleDate = saleDate;
this.employee = employee;
this.store = store;
this.customer = customer;
this.totalAmount = totalAmount;
this.paymentMethod = paymentMethod;
this.isRefund = isRefund;
@@ -102,6 +107,14 @@ public class Sale {
this.store = store;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public BigDecimal getTotalAmount() {
return totalAmount;
}
@@ -178,6 +191,7 @@ public class Sale {
", saleDate=" + saleDate +
", employee=" + employee +
", store=" + store +
", customer=" + customer +
", totalAmount=" + totalAmount +
", paymentMethod='" + paymentMethod + '\'' +
", isRefund=" + isRefund +

View File

@@ -21,6 +21,15 @@ public class User {
@Column(nullable = false)
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)
@Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)")
private Role role;
@@ -34,16 +43,19 @@ public class User {
private LocalDateTime updatedAt;
public enum Role {
STAFF, ADMIN
CUSTOMER, STAFF, ADMIN
}
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.username = username;
this.password = password;
this.email = email;
this.fullName = fullName;
this.avatarUrl = avatarUrl;
this.role = role;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
@@ -73,6 +85,30 @@ public class User {
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() {
return role;
}
@@ -116,6 +152,9 @@ public class User {
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", avatarUrl='" + avatarUrl + '\'' +
", role=" + role +
", createdAt=" + createdAt +
", 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
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
boolean existsByUsername(String username);
@Query("SELECT u FROM User u WHERE " +

View File

@@ -36,18 +36,14 @@ public class SecurityConfig {
http
.csrf(AbstractHttpConfigurer::disable)
.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("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").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/dropdowns/suppliers").hasRole("ADMIN")
.requestMatchers("/api/v1/inventory/**").hasRole("ADMIN")
.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")
.requestMatchers(HttpMethod.GET, "/api/v1/services/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll()
.anyRequest().authenticated()
)
.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:
name: petshop-backend
servlet:
multipart:
enabled: true
max-file-size: 5MB
max-request-size: 5MB
datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC}
username: ${SPRING_DATASOURCE_USERNAME:petshop}

View File

@@ -152,12 +152,14 @@ CREATE TABLE IF NOT EXISTS sale (
paymentMethod VARCHAR(50) NOT NULL,
employeeId BIGINT NOT NULL,
storeId BIGINT NOT NULL,
customerId BIGINT NULL,
isRefund BOOLEAN DEFAULT FALSE NOT NULL,
originalSaleId BIGINT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (employeeId) REFERENCES employee(employeeId),
FOREIGN KEY (storeId) REFERENCES storeLocation(storeId),
FOREIGN KEY (customerId) REFERENCES customer(customerId),
FOREIGN KEY (originalSaleId) REFERENCES sale(saleId)
);
@@ -195,7 +197,45 @@ CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) UNIQUE,
fullName VARCHAR(100),
avatarUrl VARCHAR(255),
role VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT 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)
);