Merge pull request #251 from RecentRunner/payment-fixes
Payment safety fixes
This commit is contained in:
@@ -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() {
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ public class CartController {
|
|||||||
return ResponseEntity.ok(cartService.applyCoupon(userId, storeId, request.getCouponCode()));
|
return ResponseEntity.ok(cartService.applyCoupon(userId, storeId, request.getCouponCode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/apply-points")
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
public ResponseEntity<CartResponse> applyPoints(@Valid @RequestBody ApplyPointsRequest request) {
|
||||||
|
Long userId = AuthenticationHelper.getAuthenticatedUserId();
|
||||||
|
|
||||||
|
return ResponseEntity.ok(cartService.applyPoints(userId, request.getStoreId(), request.getUseLoyaltyPoints()));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/checkout")
|
@PostMapping("/checkout")
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
public ResponseEntity<CheckoutResponse> checkout(@Valid @RequestBody CheckoutRequest request) {
|
public ResponseEntity<CheckoutResponse> checkout(@Valid @RequestBody CheckoutRequest request) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.petshop.backend.dto.cart;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public class ApplyPointsRequest {
|
||||||
|
|
||||||
|
@NotNull(message = "Store ID is required")
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
|
@NotNull(message = "useLoyaltyPoints is required")
|
||||||
|
private Boolean useLoyaltyPoints;
|
||||||
|
|
||||||
|
public Long getStoreId() {
|
||||||
|
return storeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStoreId(Long storeId) {
|
||||||
|
this.storeId = storeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getUseLoyaltyPoints() {
|
||||||
|
return useLoyaltyPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUseLoyaltyPoints(Boolean useLoyaltyPoints) {
|
||||||
|
this.useLoyaltyPoints = useLoyaltyPoints;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
}
|
}
|
||||||
@@ -35,9 +38,18 @@ public class CartResponse {
|
|||||||
public BigDecimal getDiscountAmount() { return discountAmount; }
|
public BigDecimal getDiscountAmount() { return discountAmount; }
|
||||||
public void setDiscountAmount(BigDecimal discountAmount) { this.discountAmount = discountAmount; }
|
public void setDiscountAmount(BigDecimal discountAmount) { this.discountAmount = discountAmount; }
|
||||||
|
|
||||||
|
public BigDecimal getPointsDiscountAmount() { return pointsDiscountAmount; }
|
||||||
|
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) { this.pointsDiscountAmount = pointsDiscountAmount; }
|
||||||
|
|
||||||
public BigDecimal getTotalAmount() { return totalAmount; }
|
public BigDecimal getTotalAmount() { return totalAmount; }
|
||||||
public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; }
|
public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; }
|
||||||
|
|
||||||
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 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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public class SaleRequest {
|
|||||||
|
|
||||||
private Long cartId;
|
private Long cartId;
|
||||||
|
|
||||||
|
private Boolean useLoyaltyPoints = false;
|
||||||
|
|
||||||
public Long getStoreId() {
|
public Long getStoreId() {
|
||||||
return storeId;
|
return storeId;
|
||||||
}
|
}
|
||||||
@@ -100,6 +102,14 @@ public class SaleRequest {
|
|||||||
this.cartId = cartId;
|
this.cartId = cartId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getUseLoyaltyPoints() {
|
||||||
|
return useLoyaltyPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUseLoyaltyPoints(Boolean useLoyaltyPoints) {
|
||||||
|
this.useLoyaltyPoints = useLoyaltyPoints;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
@@ -113,12 +123,13 @@ public class SaleRequest {
|
|||||||
Objects.equals(customerId, that.customerId) &&
|
Objects.equals(customerId, that.customerId) &&
|
||||||
Objects.equals(channel, that.channel) &&
|
Objects.equals(channel, that.channel) &&
|
||||||
Objects.equals(couponId, that.couponId) &&
|
Objects.equals(couponId, that.couponId) &&
|
||||||
Objects.equals(cartId, that.cartId);
|
Objects.equals(cartId, that.cartId) &&
|
||||||
|
Objects.equals(useLoyaltyPoints, that.useLoyaltyPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId, customerId, channel, couponId, cartId);
|
return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId, customerId, channel, couponId, cartId, useLoyaltyPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -133,6 +144,7 @@ public class SaleRequest {
|
|||||||
", channel='" + channel + '\'' +
|
", channel='" + channel + '\'' +
|
||||||
", couponId=" + couponId +
|
", couponId=" + couponId +
|
||||||
", cartId=" + cartId +
|
", cartId=" + cartId +
|
||||||
|
", useLoyaltyPoints=" + useLoyaltyPoints +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +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 pointsUsed;
|
||||||
private Integer pointsEarned;
|
private Integer pointsEarned;
|
||||||
private String channel;
|
private String channel;
|
||||||
private Long couponId;
|
private Long couponId;
|
||||||
@@ -127,6 +129,22 @@ public class SaleResponse {
|
|||||||
this.employeeDiscountAmount = employeeDiscountAmount;
|
this.employeeDiscountAmount = employeeDiscountAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigDecimal getLoyaltyDiscountAmount() {
|
||||||
|
return loyaltyDiscountAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoyaltyDiscountAmount(BigDecimal loyaltyDiscountAmount) {
|
||||||
|
this.loyaltyDiscountAmount = loyaltyDiscountAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getPointsUsed() {
|
||||||
|
return pointsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPointsUsed(Integer pointsUsed) {
|
||||||
|
this.pointsUsed = pointsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getPointsEarned() {
|
public Integer getPointsEarned() {
|
||||||
return pointsEarned;
|
return pointsEarned;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +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 pointsUsed = 0;
|
||||||
|
|
||||||
|
@Column(nullable = false, precision = 10, scale = 2)
|
||||||
|
private BigDecimal loyaltyDiscountAmount = BigDecimal.ZERO;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private Integer pointsEarned = 0;
|
private Integer pointsEarned = 0;
|
||||||
|
|
||||||
@@ -203,6 +209,22 @@ public class Sale {
|
|||||||
this.employeeDiscountAmount = employeeDiscountAmount;
|
this.employeeDiscountAmount = employeeDiscountAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getPointsUsed() {
|
||||||
|
return pointsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPointsUsed(Integer pointsUsed) {
|
||||||
|
this.pointsUsed = pointsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getLoyaltyDiscountAmount() {
|
||||||
|
return loyaltyDiscountAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoyaltyDiscountAmount(BigDecimal loyaltyDiscountAmount) {
|
||||||
|
this.loyaltyDiscountAmount = loyaltyDiscountAmount;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getPointsEarned() {
|
public Integer getPointsEarned() {
|
||||||
return pointsEarned;
|
return pointsEarned;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,75 @@ 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.setUseLoyaltyPoints(Boolean.TRUE.equals(cart.getPointsApplied()));
|
||||||
|
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 +393,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 +415,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 +424,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 +501,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
@@ -155,7 +163,15 @@ public class SaleService {
|
|||||||
subtotalAmount = subtotalAmount.negate();
|
subtotalAmount = subtotalAmount.negate();
|
||||||
sale.setSubtotalAmount(subtotalAmount);
|
sale.setSubtotalAmount(subtotalAmount);
|
||||||
sale.setTotalAmount(subtotalAmount);
|
sale.setTotalAmount(subtotalAmount);
|
||||||
|
sale.setCouponDiscountAmount(BigDecimal.ZERO);
|
||||||
|
sale.setEmployeeDiscountAmount(BigDecimal.ZERO);
|
||||||
|
sale.setLoyaltyDiscountAmount(BigDecimal.ZERO);
|
||||||
|
sale.setPointsUsed(0);
|
||||||
|
sale.setPointsEarned(0);
|
||||||
} 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()));
|
||||||
@@ -191,12 +207,18 @@ public class SaleService {
|
|||||||
BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount));
|
BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount));
|
||||||
sale.setEmployeeDiscountAmount(employeeDiscount);
|
sale.setEmployeeDiscountAmount(employeeDiscount);
|
||||||
|
|
||||||
BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount);
|
BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(customer, subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount), Boolean.TRUE.equals(request.getUseLoyaltyPoints()));
|
||||||
|
sale.setLoyaltyDiscountAmount(loyaltyDiscount);
|
||||||
|
sale.setPointsUsed(toPointsUsed(loyaltyDiscount));
|
||||||
|
|
||||||
|
BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount).subtract(loyaltyDiscount);
|
||||||
sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO));
|
sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO));
|
||||||
|
|
||||||
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 - sale.getPointsUsed() + sale.getPointsEarned();
|
||||||
|
customer.setLoyaltyPoints(Math.max(updatedPoints, 0));
|
||||||
userRepository.save(customer);
|
userRepository.save(customer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,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());
|
||||||
@@ -273,6 +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.setPointsUsed(sale.getPointsUsed());
|
||||||
response.setPointsEarned(sale.getPointsEarned());
|
response.setPointsEarned(sale.getPointsEarned());
|
||||||
response.setChannel(sale.getChannel());
|
response.setChannel(sale.getChannel());
|
||||||
if (sale.getCoupon() != null) {
|
if (sale.getCoupon() != null) {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
ALTER TABLE sale
|
||||||
|
ADD COLUMN pointsUsed INT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN loyaltyDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00;
|
||||||
|
|
||||||
|
ALTER TABLE cart
|
||||||
|
ADD COLUMN pointsApplied BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
ADD COLUMN pointsDiscountAmount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
ADD COLUMN checkoutPending BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
ADD COLUMN checkoutAmount DECIMAL(10, 2),
|
||||||
|
ADD COLUMN checkoutStartedAt DATETIME,
|
||||||
|
ADD COLUMN checkoutPaymentIntentId VARCHAR(255);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE sale ADD CONSTRAINT uq_sale_cart_id UNIQUE (cartId);
|
||||||
Reference in New Issue
Block a user