Integrate refund logic

This commit is contained in:
2026-04-01 19:58:53 -06:00
parent 38a71a8b59
commit 488f67289d
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; 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.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Objects; import java.util.Objects;
public class RefundRequest { public class RefundRequest {
@@ -11,6 +15,10 @@ public class RefundRequest {
@NotBlank(message = "Reason is required") @NotBlank(message = "Reason is required")
private String reason; private String reason;
@NotEmpty(message = "At least one item is required")
@Valid
private List<SaleItemRequest> items;
public Long getSaleId() { public Long getSaleId() {
return saleId; return saleId;
} }
@@ -27,18 +35,27 @@ public class RefundRequest {
this.reason = reason; this.reason = reason;
} }
public List<SaleItemRequest> getItems() {
return items;
}
public void setItems(List<SaleItemRequest> items) {
this.items = items;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
RefundRequest that = (RefundRequest) o; RefundRequest that = (RefundRequest) o;
return Objects.equals(saleId, that.saleId) && return Objects.equals(saleId, that.saleId) &&
Objects.equals(reason, that.reason); Objects.equals(reason, that.reason) &&
Objects.equals(items, that.items);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(saleId, reason); return Objects.hash(saleId, reason, items);
} }
@Override @Override
@@ -46,6 +63,7 @@ public class RefundRequest {
return "RefundRequest{" + return "RefundRequest{" +
"saleId=" + saleId + "saleId=" + saleId +
", reason='" + reason + '\'' + ", reason='" + reason + '\'' +
", items=" + items +
'}'; '}';
} }
} }

View File

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

View File

@@ -6,6 +6,8 @@ import org.hibernate.annotations.UpdateTimestamp;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects; import java.util.Objects;
@Entity @Entity
@@ -32,9 +34,30 @@ public class Refund {
@Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)") @Column(nullable = false, length = 20, columnDefinition = "VARCHAR(20)")
private RefundStatus status; private RefundStatus status;
@OneToMany(mappedBy = "refund", cascade = CascadeType.ALL, orphanRemoval = true)
private List<RefundItem> items = new ArrayList<>();
@CreationTimestamp @CreationTimestamp
@Column(name = "created_at", updatable = false) @Column(name = "created_at", updatable = false)
private LocalDateTime createdAt; 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 @UpdateTimestamp
@Column(name = "updated_at") @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; 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.RefundRequest;
import com.petshop.backend.dto.refund.RefundResponse; 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.Refund;
import com.petshop.backend.entity.RefundItem;
import com.petshop.backend.entity.Sale; 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.RefundRepository;
import com.petshop.backend.repository.SaleRepository; import com.petshop.backend.repository.SaleRepository;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -18,10 +24,17 @@ public class RefundService {
private final RefundRepository refundRepository; private final RefundRepository refundRepository;
private final SaleRepository saleRepository; 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.refundRepository = refundRepository;
this.saleRepository = saleRepository; this.saleRepository = saleRepository;
this.productRepository = productRepository;
this.saleService = saleService;
} }
@Transactional @Transactional
@@ -40,10 +53,30 @@ public class RefundService {
Refund refund = new Refund(); Refund refund = new Refund();
refund.setSaleId(sale.getSaleId()); refund.setSaleId(sale.getSaleId());
refund.setCustomerId(sale.getCustomer().getCustomerId()); refund.setCustomerId(sale.getCustomer().getCustomerId());
refund.setAmount(sale.getTotalAmount());
refund.setReason(request.getReason()); refund.setReason(request.getReason());
refund.setStatus(Refund.RefundStatus.PENDING); 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); Refund savedRefund = refundRepository.save(refund);
return toResponse(savedRefund); return toResponse(savedRefund);
} }
@@ -78,12 +111,36 @@ public class RefundService {
Refund refund = refundRepository.findById(id) Refund refund = refundRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Refund not found")); .orElseThrow(() -> new RuntimeException("Refund not found"));
Refund.RefundStatus newStatus;
try { try {
refund.setStatus(Refund.RefundStatus.valueOf(status.toUpperCase())); newStatus = Refund.RefundStatus.valueOf(status.toUpperCase());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
throw new RuntimeException("Invalid status: " + status); 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); Refund updatedRefund = refundRepository.save(refund);
return toResponse(updatedRefund); return toResponse(updatedRefund);
} }
@@ -97,6 +154,16 @@ public class RefundService {
} }
private RefundResponse toResponse(Refund refund) { 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( return new RefundResponse(
refund.getId(), refund.getId(),
refund.getSaleId(), refund.getSaleId(),
@@ -104,6 +171,7 @@ public class RefundService {
refund.getAmount(), refund.getAmount(),
refund.getReason(), refund.getReason(),
refund.getStatus().name(), refund.getStatus().name(),
itemResponses,
refund.getCreatedAt(), refund.getCreatedAt(),
refund.getUpdatedAt() 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]';

4004
web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff