Add sales and refunds with inventory management

This commit is contained in:
2026-03-04 17:18:53 -07:00
parent 883d1d6baa
commit 0df6d931f2
7 changed files with 351 additions and 22 deletions

View File

@@ -0,0 +1,25 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.refund.RefundRequest;
import com.petshop.backend.dto.refund.RefundResponse;
import com.petshop.backend.service.RefundService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/sales")
@RequiredArgsConstructor
public class RefundController {
private final RefundService refundService;
@PostMapping("/{saleId}/refunds")
public ResponseEntity<RefundResponse> createRefund(
@PathVariable Long saleId,
@Valid @RequestBody RefundRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(refundService.createRefund(saleId, request));
}
}

View File

@@ -0,0 +1,35 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.sale.SaleRequest;
import com.petshop.backend.dto.sale.SaleResponse;
import com.petshop.backend.service.SaleService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/sales")
@RequiredArgsConstructor
public class SaleController {
private final SaleService saleService;
@GetMapping
public ResponseEntity<Page<SaleResponse>> getAllSales(Pageable pageable) {
return ResponseEntity.ok(saleService.getAllSales(pageable));
}
@GetMapping("/{id}")
public ResponseEntity<SaleResponse> getSaleById(@PathVariable Long id) {
return ResponseEntity.ok(saleService.getSaleById(id));
}
@PostMapping
public ResponseEntity<SaleResponse> createSale(@Valid @RequestBody SaleRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(saleService.createSale(request));
}
}

View File

@@ -21,16 +21,16 @@ public class RefundResponse {
private String processedByName;
private List<RefundItemResponse> items;
private LocalDateTime createdAt;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class RefundItemResponse {
private Long id;
private Long saleItemId;
private Long productId;
private String productName;
private Integer quantity;
private BigDecimal refundAmount;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class RefundItemResponse {
private Long id;
private Long saleItemId;
private Long productId;
private String productName;
private Integer quantity;
private BigDecimal refundAmount;
}
}

View File

@@ -27,16 +27,16 @@ public class SaleResponse {
private String notes;
private List<SaleItemResponse> items;
private LocalDateTime createdAt;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class SaleItemResponse {
private Long id;
private Long productId;
private String productName;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal subtotal;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class SaleItemResponse {
private Long id;
private Long productId;
private String productName;
private Integer quantity;
private BigDecimal unitPrice;
private BigDecimal subtotal;
}
}

View File

@@ -0,0 +1,9 @@
package com.petshop.backend.repository;
import com.petshop.backend.entity.SaleItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SaleItemRepository extends JpaRepository<SaleItem, Long> {
}

View File

@@ -0,0 +1,115 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.refund.RefundRequest;
import com.petshop.backend.dto.refund.RefundResponse;
import com.petshop.backend.entity.*;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.*;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class RefundService {
private final RefundRepository refundRepository;
private final SaleRepository saleRepository;
private final SaleItemRepository saleItemRepository;
private final InventoryRepository inventoryRepository;
private final UserRepository userRepository;
@Transactional
public RefundResponse createRefund(Long saleId, RefundRequest request) {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
User processedBy = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("User not found: " + username));
Sale sale = saleRepository.findById(saleId)
.orElseThrow(() -> new ResourceNotFoundException("Sale not found with id: " + saleId));
Refund refund = new Refund();
refund.setSale(sale);
refund.setRefundDate(LocalDateTime.now());
refund.setRefundReason(request.getRefundReason());
refund.setProcessedBy(processedBy);
BigDecimal totalRefundAmount = BigDecimal.ZERO;
List<RefundItem> refundItems = new ArrayList<>();
for (var itemRequest : request.getItems()) {
SaleItem saleItem = saleItemRepository.findById(itemRequest.getSaleItemId())
.orElseThrow(() -> new ResourceNotFoundException("Sale item not found with id: " + itemRequest.getSaleItemId()));
if (!saleItem.getSale().getId().equals(saleId)) {
throw new BusinessException("Sale item " + itemRequest.getSaleItemId() + " does not belong to sale " + saleId);
}
if (itemRequest.getQuantity() > saleItem.getQuantity()) {
throw new BusinessException("Refund quantity (" + itemRequest.getQuantity() +
") exceeds original sale quantity (" + saleItem.getQuantity() + ") for product: " + saleItem.getProduct().getProductName());
}
Inventory inventory = inventoryRepository.findByProductIdAndStoreId(
saleItem.getProduct().getId(),
sale.getStore().getId())
.orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " +
saleItem.getProduct().getId() + " at store " + sale.getStore().getId()));
inventory.setQuantity(inventory.getQuantity() + itemRequest.getQuantity());
inventory.setLastRestocked(LocalDateTime.now());
inventoryRepository.save(inventory);
BigDecimal itemRefundAmount = saleItem.getUnitPrice().multiply(BigDecimal.valueOf(itemRequest.getQuantity()));
RefundItem refundItem = new RefundItem();
refundItem.setRefund(refund);
refundItem.setSaleItem(saleItem);
refundItem.setQuantity(itemRequest.getQuantity());
refundItem.setRefundAmount(itemRefundAmount);
refundItems.add(refundItem);
totalRefundAmount = totalRefundAmount.add(itemRefundAmount);
}
refund.setRefundAmount(totalRefundAmount);
refund.setItems(refundItems);
Refund savedRefund = refundRepository.save(refund);
return mapToResponse(savedRefund);
}
private RefundResponse mapToResponse(Refund refund) {
RefundResponse response = new RefundResponse();
response.setId(refund.getId());
response.setSaleId(refund.getSale().getId());
response.setRefundDate(refund.getRefundDate());
response.setRefundAmount(refund.getRefundAmount());
response.setRefundReason(refund.getRefundReason());
response.setProcessedBy(refund.getProcessedBy().getId());
response.setProcessedByName(refund.getProcessedBy().getFullName());
response.setCreatedAt(refund.getCreatedAt());
List<RefundResponse.RefundItemResponse> itemResponses = new ArrayList<>();
for (RefundItem item : refund.getItems()) {
RefundResponse.RefundItemResponse itemResponse = new RefundResponse.RefundItemResponse();
itemResponse.setId(item.getId());
itemResponse.setSaleItemId(item.getSaleItem().getId());
itemResponse.setProductId(item.getSaleItem().getProduct().getId());
itemResponse.setProductName(item.getSaleItem().getProduct().getProductName());
itemResponse.setQuantity(item.getQuantity());
itemResponse.setRefundAmount(item.getRefundAmount());
itemResponses.add(itemResponse);
}
response.setItems(itemResponses);
return response;
}
}

View File

@@ -0,0 +1,145 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.sale.SaleRequest;
import com.petshop.backend.dto.sale.SaleResponse;
import com.petshop.backend.entity.*;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.*;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@RequiredArgsConstructor
public class SaleService {
private final SaleRepository saleRepository;
private final ProductRepository productRepository;
private final CustomerRepository customerRepository;
private final StoreRepository storeRepository;
private final InventoryRepository inventoryRepository;
private final UserRepository userRepository;
public Page<SaleResponse> getAllSales(Pageable pageable) {
return saleRepository.findAll(pageable).map(this::mapToResponse);
}
public SaleResponse getSaleById(Long id) {
Sale sale = saleRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Sale not found with id: " + id));
return mapToResponse(sale);
}
@Transactional
public SaleResponse createSale(SaleRequest request) {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
User employee = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("User not found: " + username));
Store store = storeRepository.findById(request.getStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
Customer customer = null;
if (request.getCustomerId() != null) {
customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
}
Sale sale = new Sale();
sale.setSaleDate(LocalDateTime.now());
sale.setEmployee(employee);
sale.setCustomer(customer);
sale.setStore(store);
sale.setPaymentMethod(request.getPaymentMethod());
sale.setTax(request.getTax());
sale.setNotes(request.getNotes());
BigDecimal subtotal = BigDecimal.ZERO;
List<SaleItem> saleItems = new ArrayList<>();
for (var itemRequest : request.getItems()) {
Product product = productRepository.findById(itemRequest.getProductId())
.orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + itemRequest.getProductId()));
Inventory inventory = inventoryRepository.findByProductIdAndStoreId(itemRequest.getProductId(), request.getStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Inventory not found for product " + itemRequest.getProductId() + " at store " + request.getStoreId()));
if (inventory.getQuantity() < itemRequest.getQuantity()) {
throw new BusinessException("Insufficient stock for product: " + product.getProductName() +
". Available: " + inventory.getQuantity() + ", requested: " + itemRequest.getQuantity());
}
inventory.setQuantity(inventory.getQuantity() - itemRequest.getQuantity());
inventoryRepository.save(inventory);
BigDecimal unitPrice = product.getProductPrice();
BigDecimal itemSubtotal = unitPrice.multiply(BigDecimal.valueOf(itemRequest.getQuantity()));
SaleItem saleItem = new SaleItem();
saleItem.setSale(sale);
saleItem.setProduct(product);
saleItem.setQuantity(itemRequest.getQuantity());
saleItem.setUnitPrice(unitPrice);
saleItem.setSubtotal(itemSubtotal);
saleItems.add(saleItem);
subtotal = subtotal.add(itemSubtotal);
}
sale.setSubtotal(subtotal);
sale.setTotal(subtotal.add(sale.getTax()));
sale.setItems(saleItems);
Sale savedSale = saleRepository.save(sale);
return mapToResponse(savedSale);
}
private SaleResponse mapToResponse(Sale sale) {
SaleResponse response = new SaleResponse();
response.setId(sale.getId());
response.setSaleDate(sale.getSaleDate());
response.setEmployeeId(sale.getEmployee().getId());
response.setEmployeeName(sale.getEmployee().getFullName());
if (sale.getCustomer() != null) {
response.setCustomerId(sale.getCustomer().getId());
response.setCustomerName(sale.getCustomer().getCustomerName());
}
if (sale.getStore() != null) {
response.setStoreId(sale.getStore().getId());
response.setStoreName(sale.getStore().getStoreName());
}
response.setSubtotal(sale.getSubtotal());
response.setTax(sale.getTax());
response.setTotal(sale.getTotal());
response.setPaymentMethod(sale.getPaymentMethod());
response.setNotes(sale.getNotes());
response.setCreatedAt(sale.getCreatedAt());
List<SaleResponse.SaleItemResponse> itemResponses = new ArrayList<>();
for (SaleItem item : sale.getItems()) {
SaleResponse.SaleItemResponse itemResponse = new SaleResponse.SaleItemResponse();
itemResponse.setId(item.getId());
itemResponse.setProductId(item.getProduct().getId());
itemResponse.setProductName(item.getProduct().getProductName());
itemResponse.setQuantity(item.getQuantity());
itemResponse.setUnitPrice(item.getUnitPrice());
itemResponse.setSubtotal(item.getSubtotal());
itemResponses.add(itemResponse);
}
response.setItems(itemResponses);
return response;
}
}