Add sales and refunds with inventory management
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user