Integrate refund logic

This commit is contained in:
2026-04-01 19:58:53 -06:00
parent a45a437d39
commit 3efb285e17
8 changed files with 4338 additions and 7 deletions

View File

@@ -0,0 +1,62 @@
package com.petshop.backend.dto.refund;
import java.math.BigDecimal;
public class RefundItemResponse {
private Long id;
private Long prodId;
private String prodName;
private Integer quantity;
private BigDecimal unitPrice;
public RefundItemResponse() {
}
public RefundItemResponse(Long id, Long prodId, String prodName, Integer quantity, BigDecimal unitPrice) {
this.id = id;
this.prodId = prodId;
this.prodName = prodName;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getProdId() {
return prodId;
}
public void setProdId(Long prodId) {
this.prodId = prodId;
}
public String getProdName() {
return prodName;
}
public void setProdName(String prodName) {
this.prodName = prodName;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
}
}

View File

@@ -1,7 +1,11 @@
package com.petshop.backend.dto.refund;
import com.petshop.backend.dto.sale.SaleItemRequest;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Objects;
public class RefundRequest {
@@ -11,6 +15,10 @@ public class RefundRequest {
@NotBlank(message = "Reason is required")
private String reason;
@NotEmpty(message = "At least one item is required")
@Valid
private List<SaleItemRequest> items;
public Long getSaleId() {
return saleId;
}
@@ -27,18 +35,27 @@ public class RefundRequest {
this.reason = reason;
}
public List<SaleItemRequest> getItems() {
return items;
}
public void setItems(List<SaleItemRequest> items) {
this.items = items;
}
@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);
Objects.equals(reason, that.reason) &&
Objects.equals(items, that.items);
}
@Override
public int hashCode() {
return Objects.hash(saleId, reason);
return Objects.hash(saleId, reason, items);
}
@Override
@@ -46,6 +63,7 @@ public class RefundRequest {
return "RefundRequest{" +
"saleId=" + saleId +
", reason='" + reason + '\'' +
", items=" + items +
'}';
}
}

View File

@@ -2,6 +2,7 @@ package com.petshop.backend.dto.refund;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
public class RefundResponse {
@@ -11,22 +12,32 @@ public class RefundResponse {
private BigDecimal amount;
private String reason;
private String status;
private List<RefundItemResponse> items;
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) {
public RefundResponse(Long id, Long saleId, Long customerId, BigDecimal amount, String reason, String status, List<RefundItemResponse> items, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.saleId = saleId;
this.customerId = customerId;
this.amount = amount;
this.reason = reason;
this.status = status;
this.items = items;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// ...
public List<RefundItemResponse> getItems() {
return items;
}
public void setItems(List<RefundItemResponse> items) {
this.items = items;
}
public Long getId() {
return id;

View File

@@ -6,6 +6,8 @@ import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Entity
@@ -32,9 +34,30 @@ public class Refund {
@Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)")
private RefundStatus status;
@OneToMany(mappedBy = "refund", cascade = CascadeType.ALL, orphanRemoval = true)
private List<RefundItem> items = new ArrayList<>();
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
// ...
public List<RefundItem> getItems() {
return items;
}
public void setItems(List<RefundItem> items) {
this.items = items;
}
public void addItem(RefundItem item) {
items.add(item);
item.setRefund(this);
}
public void removeItem(RefundItem item) {
items.remove(item);
item.setRefund(null);
}
@UpdateTimestamp
@Column(name = "updated_at")

View File

@@ -0,0 +1,112 @@
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_item")
public class RefundItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "refund_id", nullable = false)
private Refund refund;
@ManyToOne
@JoinColumn(name = "prod_id", nullable = false)
private Product product;
@Column(nullable = false)
private Integer quantity;
@Column(name = "unit_price", nullable = false, precision = 10, scale = 2)
private BigDecimal unitPrice;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
public RefundItem() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Refund getRefund() {
return refund;
}
public void setRefund(Refund refund) {
this.refund = refund;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
}
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;
RefundItem that = (RefundItem) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}

View File

@@ -1,15 +1,21 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.refund.RefundItemResponse;
import com.petshop.backend.dto.refund.RefundRequest;
import com.petshop.backend.dto.refund.RefundResponse;
import com.petshop.backend.dto.sale.SaleRequest;
import com.petshop.backend.entity.Product;
import com.petshop.backend.entity.Refund;
import com.petshop.backend.entity.RefundItem;
import com.petshop.backend.entity.Sale;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.ProductRepository;
import com.petshop.backend.repository.RefundRepository;
import com.petshop.backend.repository.SaleRepository;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
@@ -18,10 +24,17 @@ public class RefundService {
private final RefundRepository refundRepository;
private final SaleRepository saleRepository;
private final ProductRepository productRepository;
private final SaleService saleService;
public RefundService(RefundRepository refundRepository, SaleRepository saleRepository) {
public RefundService(RefundRepository refundRepository,
SaleRepository saleRepository,
ProductRepository productRepository,
@Lazy SaleService saleService) {
this.refundRepository = refundRepository;
this.saleRepository = saleRepository;
this.productRepository = productRepository;
this.saleService = saleService;
}
@Transactional
@@ -40,10 +53,30 @@ public class RefundService {
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);
BigDecimal totalAmount = BigDecimal.ZERO;
for (var itemRequest : request.getItems()) {
Product product = productRepository.findById(itemRequest.getProdId())
.orElseThrow(() -> new RuntimeException("Product not found: " + itemRequest.getProdId()));
BigDecimal unitPrice = sale.getItems().stream()
.filter(item -> item.getProduct().getProdId().equals(itemRequest.getProdId()))
.findFirst()
.map(item -> item.getUnitPrice())
.orElseThrow(() -> new RuntimeException("Product " + itemRequest.getProdId() + " was not in original sale"));
RefundItem refundItem = new RefundItem();
refundItem.setProduct(product);
refundItem.setQuantity(itemRequest.getQuantity());
refundItem.setUnitPrice(unitPrice);
refund.addItem(refundItem);
totalAmount = totalAmount.add(unitPrice.multiply(BigDecimal.valueOf(itemRequest.getQuantity())));
}
refund.setAmount(totalAmount);
Refund savedRefund = refundRepository.save(refund);
return toResponse(savedRefund);
}
@@ -78,12 +111,36 @@ public class RefundService {
Refund refund = refundRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Refund not found"));
Refund.RefundStatus newStatus;
try {
refund.setStatus(Refund.RefundStatus.valueOf(status.toUpperCase()));
newStatus = Refund.RefundStatus.valueOf(status.toUpperCase());
} catch (IllegalArgumentException e) {
throw new RuntimeException("Invalid status: " + status);
}
if (refund.getStatus() == Refund.RefundStatus.PENDING && newStatus == Refund.RefundStatus.APPROVED) {
Sale originalSale = saleRepository.findById(refund.getSaleId())
.orElseThrow(() -> new RuntimeException("Original sale not found"));
SaleRequest saleRequest = new SaleRequest();
saleRequest.setStoreId(originalSale.getStore().getStoreId());
saleRequest.setCustomerId(refund.getCustomerId());
saleRequest.setOriginalSaleId(refund.getSaleId());
saleRequest.setIsRefund(true);
saleRequest.setPaymentMethod("Card");
saleRequest.setItems(refund.getItems().stream()
.map(item -> {
var ir = new com.petshop.backend.dto.sale.SaleItemRequest();
ir.setProdId(item.getProduct().getProdId());
ir.setQuantity(item.getQuantity());
return ir;
})
.collect(Collectors.toList()));
saleService.createSale(saleRequest);
}
refund.setStatus(newStatus);
Refund updatedRefund = refundRepository.save(refund);
return toResponse(updatedRefund);
}
@@ -97,6 +154,16 @@ public class RefundService {
}
private RefundResponse toResponse(Refund refund) {
List<RefundItemResponse> itemResponses = refund.getItems().stream()
.map(item -> new RefundItemResponse(
item.getId(),
item.getProduct().getProdId(),
item.getProduct().getProdName(),
item.getQuantity(),
item.getUnitPrice()
))
.collect(Collectors.toList());
return new RefundResponse(
refund.getId(),
refund.getSaleId(),
@@ -104,6 +171,7 @@ public class RefundService {
refund.getAmount(),
refund.getReason(),
refund.getStatus().name(),
itemResponses,
refund.getCreatedAt(),
refund.getUpdatedAt()
);

View File

@@ -0,0 +1,33 @@
-- Consolidated Updates: Phone Normalization and Refund Items
-- 1. Create refund_item table
CREATE TABLE IF NOT EXISTS refund_item (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
refund_id BIGINT NOT NULL,
prod_id BIGINT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (refund_id) REFERENCES refund(id) ON DELETE CASCADE,
FOREIGN KEY (prod_id) REFERENCES product(prodId)
);
-- 2. Normalize existing phone numbers (MySQL Set-based)
UPDATE users
SET phone = CONCAT('(', SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -10, 3), ') ',
SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -7, 3), '-',
SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -4))
WHERE phone REGEXP '[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9]';
UPDATE supplier
SET supPhone = CONCAT('(', SUBSTRING(REGEXP_REPLACE(supPhone, '[^0-9]', ''), -10, 3), ') ',
SUBSTRING(REGEXP_REPLACE(supPhone, '[^0-9]', ''), -7, 3), '-',
SUBSTRING(REGEXP_REPLACE(supPhone, '[^0-9]', ''), -4))
WHERE supPhone REGEXP '[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9]';
UPDATE storeLocation
SET phone = CONCAT('(', SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -10, 3), ') ',
SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -7, 3), '-',
SUBSTRING(REGEXP_REPLACE(phone, '[^0-9]', ''), -4))
WHERE phone REGEXP '[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9].*[0-9]';